QuickImage – Rev 1

Subversion Repositories:
Rev:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic;
using System.Windows.Forms;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using static System.Windows.Forms.AxHost;
using static Microsoft.WindowsAPICodePack.Shell.PropertySystem.SystemProperties.System;
using Message = System.Windows.Forms.Message;

namespace QuickImage.Utilities.Controls
{

    /// <summary>
    /// 
    /// </summary>
    /// <remarks>https://stackoverflow.com/questions/1330050/collapsible-listview</remarks>
    /// <remarks>https://stackoverflow.com/questions/41816335/listview-group-header-click-how-to-add-a-context-menu-to-listview-group-header</remarks>
    public class ListViewCollapsible : ListView
    {
        public event EventHandler<ListViewGroup> GroupHeaderClick;
        private Dictionary<int, GroupState> groupStates = new Dictionary<int, GroupState>();
        private Dictionary<int, GroupState> groupStateMasks = new Dictionary<int, GroupState>();
        
        private const int LVM_FIRST = 0x1000;
        // ListView messages
        private const int LVM_SETGROUPINFO = LVM_FIRST + 147;

        private const int LVM_GETGROUPINFO = LVM_FIRST + 149;
        private const int LVM_GETGROUPSTATE = LVM_FIRST + 92;
        // ListView messages Setinfo on Group
        private const int WM_LBUTTONUP = 0x202;
        private const int LVM_HITTEST = LVM_FIRST + 18;
        private const int LVM_SUBITEMHITTEST = LVM_FIRST + 57;

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, int lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO ht);
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageString(IntPtr hWnd, int Msg, int wParam, string lParam);
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageIUnknown(IntPtr hWnd, int msg,
            [MarshalAs(UnmanagedType.IUnknown)] object wParam, int lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP lParam);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP2 lParam);

        public ListViewCollapsible()
        {
            //Activate double buffering
            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);

            //Enable the OnNotifyMessage event so we get a chance to filter out 
            // Windows messages before they get to the form's WndProc
            SetStyle(ControlStyles.EnableNotifyMessage, true);
        }

        public bool GetCollapsed(ListViewGroup group)
        {
            //var index = group.ListView.Groups.IndexOf(group);
            var index = ExtractID(group);
            return GetOneState(this, index, GroupState.LVGS_COLLAPSED);
        }

        public void SetCollapsed(ListViewGroup group, bool value)
        {
            //var index = group.ListView.Groups.IndexOf(group);
            var index = ExtractID(group);
            SetOneState(this, index, value, GroupState.LVGS_COLLAPSED);
        }
        
        private GroupState GetState(ListViewCollapsible olv, int groupId)
        {
            return (GroupState)SendMessage(olv.Handle, LVM_GETGROUPSTATE, groupId, (int)GroupState.LVGS_ALL);
        }

        private int SetGroupInfo(ListViewCollapsible olv, int groupId, LVGROUP2 group)
        {
            return (int)SendMessage(olv.Handle, LVM_SETGROUPINFO, groupId, ref group);
        }
        
        private int SetState(ListViewCollapsible olv, int groupId, GroupState newState, GroupState mask)
        {
            LVGROUP2 group = new LVGROUP2();
            group.cbSize = ((uint)Marshal.SizeOf(typeof(LVGROUP2)));
            group.mask = (uint)GroupMask.LVGF_STATE;
            group.state = (uint)newState;
            group.stateMask = (uint)mask;
            return SetGroupInfo(olv,groupId, group);
        }

        private void SetOneState(ListViewCollapsible olv, int groupId, bool value, GroupState mask)
        {
            if (!groupStateMasks.ContainsKey(groupId))
            {
                groupStateMasks.Add(groupId, GetState(olv, groupId));
            }

            groupStateMasks[groupId] ^= mask;

            if (!groupStates.ContainsKey(groupId))
            {
                groupStates.Add(groupId, GetState(olv, groupId));
            }

            if (value)
            {
                groupStates[groupId] ^= mask;
            }
            else
            {
                groupStates[groupId] &= ~mask;
            }

            if (Created)
                SetState(olv, groupId, groupStates[groupId], mask);
        }

        private bool GetOneState(ListViewCollapsible olv, int groupId, GroupState mask)
        {
            if (Created)
            {
                if (!groupStates.ContainsKey(groupId))
                {
                    groupStates.Add(groupId, GetState(olv, groupId));
                    return (groupStates[groupId] & mask) == mask;

                }

                groupStates[groupId] = GetState(olv, groupId);
            }

            return (groupStates[groupId] & mask) == mask;
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);

            var group = TestGroupHit(e);
            if (group == null)
            {
                return;
            }

            switch (e.Clicks)
            {
                case 1:
                    if (GroupHeaderClick != null)
                    {
                        GroupHeaderClick(this, group);
                    }
                    break;
            }
        }

        private ListViewGroup TestGroupHit(MouseEventArgs e)
        {
            var ht = new LVHITTESTINFO();
            ht.pt.x = e.X;
            ht.pt.y = e.Y;
            var msg = View == System.Windows.Forms.View.Details ? LVM_SUBITEMHITTEST : LVM_HITTEST;
            var value = (int)SendMessage(Handle, msg, -1, ref ht);

            if (value != -1 && ht.flags.HasFlag(LVHITTESTFLAGS.LVHT_EX_GROUP_HEADER))
            {
                return FindGroupById(value);
            }

            return null;
        }

        private ListViewGroup FindGroupById(int id)
        {
            foreach (ListViewGroup group in Groups)
            {
                if (ExtractID(group) == id)
                {
                    return group;
                }
            }

            return null;
        }

        public static int ExtractID(ListViewGroup group)
        {
            try
            {
                return (int)group
                    .GetType()
                    .GetProperty("ID", BindingFlags.NonPublic | BindingFlags.Instance)
                    .GetValue(group, new object[0]);
            }
            catch
            {
                return -1;
            }
        }

        protected override void OnNotifyMessage(Message m)
        {
            //Filter out the WM_ERASEBKGND message
            if (m.Msg != 0x14)
            {
                base.OnNotifyMessage(m);
            }
        }

        /// <summary>
        /// convert the IntPtr LParam to an Point.
        /// </summary>
        private static POINT LParamToPoint(IntPtr lparam)
        {
            return new POINT(lparam.ToInt32() & 0xFFFF, lparam.ToInt32() >> 16);
        }

        #region Natives

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/bb774754%28v=VS.85%29.aspx
        /// </summary>
        [Flags]
        public enum LVHITTESTFLAGS : uint
        {
            LVHT_NOWHERE = 0x00000001,
            LVHT_ONITEMICON = 0x00000002,
            LVHT_ONITEMLABEL = 0x00000004,
            LVHT_ONITEMSTATEICON = 0x00000008,
            LVHT_ONITEM = (LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON),
            LVHT_ABOVE = 0x00000008,
            LVHT_BELOW = 0x00000010,
            LVHT_TORIGHT = 0x00000020,
            LVHT_TOLEFT = 0x00000040,
            // Vista/Win7+ only
            LVHT_EX_GROUP_HEADER = 0x10000000,
            LVHT_EX_GROUP_FOOTER = 0x20000000,
            LVHT_EX_GROUP_COLLAPSE = 0x40000000,
            LVHT_EX_GROUP_BACKGROUND = 0x80000000,
            LVHT_EX_GROUP_STATEICON = 0x01000000,
            LVHT_EX_GROUP_SUBSETLINK = 0x02000000,
        }

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/dd162805%28v=VS.85%29.aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public POINT(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
            public int x;
            public int y;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct LVGROUP
        {
            public uint cbSize;
            public uint mask;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszHeader;
            public int cchHeader;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszFooter;
            public int cchFooter;
            public int iGroupId;
            public uint stateMask;
            public uint state;
            public uint uAlign;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct LVGROUP2
        {
            public uint cbSize;
            public uint mask;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszHeader;
            public uint cchHeader;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszFooter;
            public int cchFooter;
            public int iGroupId;
            public uint stateMask;
            public uint state;
            public uint uAlign;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszSubtitle;
            public uint cchSubtitle;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszTask;
            public uint cchTask;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszDescriptionTop;
            public uint cchDescriptionTop;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszDescriptionBottom;
            public uint cchDescriptionBottom;
            public int iTitleImage;
            public int iExtendedImage;
            public int iFirstItem;         // Read only
            public int cItems;             // Read only
            [MarshalAs(UnmanagedType.LPTStr)]
            public string pszSubsetTitle;     // NULL if group is not subset
            public uint cchSubsetTitle;
        }

        /// <summary>
        /// These values indicate what is the state of the group. These values
        /// are taken directly from the SDK and many are not used by ObjectListView.
        /// </summary>
        [Flags]
        public enum GroupState
        {
            /// <summary>
            /// Normal
            /// </summary>
            LVGS_NORMAL = 0x0,

            /// <summary>
            /// Collapsed
            /// </summary>
            LVGS_COLLAPSED = 0x1,

            /// <summary>
            /// Hidden
            /// </summary>
            LVGS_HIDDEN = 0x2,

            /// <summary>
            /// NoHeader
            /// </summary>
            LVGS_NOHEADER = 0x4,

            /// <summary>
            /// Can be collapsed
            /// </summary>
            LVGS_COLLAPSIBLE = 0x8,

            /// <summary>
            /// Has focus
            /// </summary>
            LVGS_FOCUSED = 0x10,

            /// <summary>
            /// Is Selected
            /// </summary>
            LVGS_SELECTED = 0x20,

            /// <summary>
            /// Is subsetted
            /// </summary>
            LVGS_SUBSETED = 0x40,

            /// <summary>
            /// Subset link has focus
            /// </summary>
            LVGS_SUBSETLINKFOCUSED = 0x80,

            /// <summary>
            /// All styles
            /// </summary>
            LVGS_ALL = 0xFFFF
        }

        /// <summary>
        /// This mask indicates which members of a LVGROUP have valid data. These values
        /// are taken directly from the SDK and many are not used by ObjectListView.
        /// </summary>
        [Flags]
        public enum GroupMask
        {
            /// <summary>
            /// No mask
            /// </summary>
            LVGF_NONE = 0,

            /// <summary>
            /// Group has header
            /// </summary>
            LVGF_HEADER = 1,

            /// <summary>
            /// Group has footer
            /// </summary>
            LVGF_FOOTER = 2,

            /// <summary>
            /// Group has state
            /// </summary>
            LVGF_STATE = 4,

            /// <summary>
            /// 
            /// </summary>
            LVGF_ALIGN = 8,

            /// <summary>
            /// 
            /// </summary>
            LVGF_GROUPID = 0x10,

            /// <summary>
            /// pszSubtitle is valid
            /// </summary>
            LVGF_SUBTITLE = 0x00100,

            /// <summary>
            /// pszTask is valid
            /// </summary>
            LVGF_TASK = 0x00200,

            /// <summary>
            /// pszDescriptionTop is valid
            /// </summary>
            LVGF_DESCRIPTIONTOP = 0x00400,

            /// <summary>
            /// pszDescriptionBottom is valid
            /// </summary>
            LVGF_DESCRIPTIONBOTTOM = 0x00800,

            /// <summary>
            /// iTitleImage is valid
            /// </summary>
            LVGF_TITLEIMAGE = 0x01000,

            /// <summary>
            /// iExtendedImage is valid
            /// </summary>
            LVGF_EXTENDEDIMAGE = 0x02000,

            /// <summary>
            /// iFirstItem and cItems are valid
            /// </summary>
            LVGF_ITEMS = 0x04000,

            /// <summary>
            /// pszSubsetTitle is valid
            /// </summary>
            LVGF_SUBSET = 0x08000,

            /// <summary>
            /// readonly, cItems holds count of items in visible subset, iFirstItem is valid
            /// </summary>
            LVGF_SUBSETITEMS = 0x10000
        }

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/bb774754%28v=VS.85%29.aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct LVHITTESTINFO
        {
            public POINT pt;
            public LVHITTESTFLAGS flags;
            public int iItem;
            public int iSubItem;
            // Vista/Win7+
            public int iGroup;
        }

        #endregion

    }
}