/*
* Groups - Enhancements to the normal ListViewGroup
*
* Author: Phillip Piper
* Date: 22/08/2009 6:03PM
*
* Change log:
* v2.3
* 2009-09-09 JPP - Added Collapsed and Collapsible properties
* 2009-09-01 JPP - Cleaned up code, added more docs
* - Works under VS2005 again
* 2009-08-22 JPP - Initial version
*
* To do:
* - Implement subseting
* - Implement footer items
*
* Copyright (C) 2009-2014 Phillip Piper
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace BrightIdeasSoftware
{
///
/// 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.
///
[Flags]
public enum GroupState
{
///
/// Normal
///
LVGS_NORMAL = 0x0,
///
/// Collapsed
///
LVGS_COLLAPSED = 0x1,
///
/// Hidden
///
LVGS_HIDDEN = 0x2,
///
/// NoHeader
///
LVGS_NOHEADER = 0x4,
///
/// Can be collapsed
///
LVGS_COLLAPSIBLE = 0x8,
///
/// Has focus
///
LVGS_FOCUSED = 0x10,
///
/// Is Selected
///
LVGS_SELECTED = 0x20,
///
/// Is subsetted
///
LVGS_SUBSETED = 0x40,
///
/// Subset link has focus
///
LVGS_SUBSETLINKFOCUSED = 0x80,
///
/// All styles
///
LVGS_ALL = 0xFFFF
}
///
/// 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.
///
[Flags]
public enum GroupMask
{
///
/// No mask
///
LVGF_NONE = 0,
///
/// Group has header
///
LVGF_HEADER = 1,
///
/// Group has footer
///
LVGF_FOOTER = 2,
///
/// Group has state
///
LVGF_STATE = 4,
///
///
///
LVGF_ALIGN = 8,
///
///
///
LVGF_GROUPID = 0x10,
///
/// pszSubtitle is valid
///
LVGF_SUBTITLE = 0x00100,
///
/// pszTask is valid
///
LVGF_TASK = 0x00200,
///
/// pszDescriptionTop is valid
///
LVGF_DESCRIPTIONTOP = 0x00400,
///
/// pszDescriptionBottom is valid
///
LVGF_DESCRIPTIONBOTTOM = 0x00800,
///
/// iTitleImage is valid
///
LVGF_TITLEIMAGE = 0x01000,
///
/// iExtendedImage is valid
///
LVGF_EXTENDEDIMAGE = 0x02000,
///
/// iFirstItem and cItems are valid
///
LVGF_ITEMS = 0x04000,
///
/// pszSubsetTitle is valid
///
LVGF_SUBSET = 0x08000,
///
/// readonly, cItems holds count of items in visible subset, iFirstItem is valid
///
LVGF_SUBSETITEMS = 0x10000
}
///
/// This mask indicates which members of a GROUPMETRICS structure are valid
///
[Flags]
public enum GroupMetricsMask
{
///
///
///
LVGMF_NONE = 0,
///
///
///
LVGMF_BORDERSIZE = 1,
///
///
///
LVGMF_BORDERCOLOR = 2,
///
///
///
LVGMF_TEXTCOLOR = 4
}
///
/// Instances of this class enhance the capabilities of a normal ListViewGroup,
/// enabling the functionality that was released in v6 of the common controls.
///
///
///
/// In this implementation (2009-09), these objects are essentially passive.
/// Setting properties does not automatically change the associated group in
/// the listview. Collapsed and Collapsible are two exceptions to this and
/// give immediate results.
///
///
/// This really should be a subclass of ListViewGroup, but that class is
/// sealed (why is that?). So this class provides the same interface as a
/// ListViewGroup, plus many other new properties.
///
///
public class OLVGroup
{
#region Creation
///
/// Create an OLVGroup
///
public OLVGroup() : this("Default group header") {
}
///
/// Create a group with the given title
///
/// Title of the group
public OLVGroup(string header) {
this.Header = header;
this.Id = OLVGroup.nextId++;
this.TitleImage = -1;
this.ExtendedImage = -1;
}
private static int nextId;
#endregion
#region Public properties
///
/// Gets or sets the bottom description of the group
///
///
/// Descriptions only appear when group is centered and there is a title image
///
public string BottomDescription {
get { return this.bottomDescription; }
set { this.bottomDescription = value; }
}
private string bottomDescription;
///
/// Gets or sets whether or not this group is collapsed
///
public bool Collapsed {
get { return this.GetOneState(GroupState.LVGS_COLLAPSED); }
set { this.SetOneState(value, GroupState.LVGS_COLLAPSED); }
}
///
/// Gets or sets whether or not this group can be collapsed
///
public bool Collapsible {
get { return this.GetOneState(GroupState.LVGS_COLLAPSIBLE); }
set { this.SetOneState(value, GroupState.LVGS_COLLAPSIBLE); }
}
///
/// Gets or sets some representation of the contents of this group
///
/// This is user defined (like Tag)
public IList Contents {
get { return this.contents; }
set { this.contents = value; }
}
private IList contents;
///
/// Gets whether this group has been created.
///
public bool Created {
get { return this.ListView != null; }
}
///
/// Gets or sets the int or string that will select the extended image to be shown against the title
///
public object ExtendedImage {
get { return this.extendedImage; }
set { this.extendedImage = value; }
}
private object extendedImage;
///
/// Gets or sets the footer of the group
///
public string Footer {
get { return this.footer; }
set { this.footer = value; }
}
private string footer;
///
/// Gets the internal id of our associated ListViewGroup.
///
public int GroupId {
get {
if (this.ListViewGroup == null)
return this.Id;
// Use reflection to get around the access control on the ID property
if (OLVGroup.groupIdPropInfo == null) {
OLVGroup.groupIdPropInfo = typeof(ListViewGroup).GetProperty("ID",
BindingFlags.NonPublic | BindingFlags.Instance);
System.Diagnostics.Debug.Assert(OLVGroup.groupIdPropInfo != null);
}
int? groupId = OLVGroup.groupIdPropInfo.GetValue(this.ListViewGroup, null) as int?;
return groupId.HasValue ? groupId.Value : -1;
}
}
private static PropertyInfo groupIdPropInfo;
///
/// Gets or sets the header of the group
///
public string Header {
get { return this.header; }
set { this.header = value; }
}
private string header;
///
/// Gets or sets the horizontal alignment of the group header
///
public HorizontalAlignment HeaderAlignment {
get { return this.headerAlignment; }
set { this.headerAlignment = value; }
}
private HorizontalAlignment headerAlignment;
///
/// Gets or sets the internally created id of the group
///
public int Id {
get { return this.id; }
set { this.id = value; }
}
private int id;
///
/// Gets or sets ListViewItems that are members of this group
///
/// Listener of the BeforeCreatingGroups event can populate this collection.
/// It is only used on non-virtual lists.
public IList Items {
get { return this.items; }
set { this.items = value; }
}
private IList items = new List();
///
/// Gets or sets the key that was used to partition objects into this group
///
/// This is user defined (like Tag)
public object Key {
get { return this.key; }
set { this.key = value; }
}
private object key;
///
/// Gets the ObjectListView that this group belongs to
///
/// If this is null, the group has not yet been created.
public ObjectListView ListView {
get { return this.listView; }
protected set { this.listView = value; }
}
private ObjectListView listView;
///
/// Gets or sets the name of the group
///
/// As of 2009-09-01, this property is not used.
public string Name {
get { return this.name; }
set { this.name = value; }
}
private string name;
///
/// Gets or sets whether this group is focused
///
public bool Focused
{
get { return this.GetOneState(GroupState.LVGS_FOCUSED); }
set { this.SetOneState(value, GroupState.LVGS_FOCUSED); }
}
///
/// Gets or sets whether this group is selected
///
public bool Selected
{
get { return this.GetOneState(GroupState.LVGS_SELECTED); }
set { this.SetOneState(value, GroupState.LVGS_SELECTED); }
}
///
/// Gets or sets the text that will show that this group is subsetted
///
///
/// As of WinSDK v7.0, subsetting of group is officially unimplemented.
/// We can get around this using undocumented interfaces and may do so.
///
public string SubsetTitle {
get { return this.subsetTitle; }
set { this.subsetTitle = value; }
}
private string subsetTitle;
///
/// Gets or set the subtitleof the task
///
public string Subtitle {
get { return this.subtitle; }
set { this.subtitle = value; }
}
private string subtitle;
///
/// Gets or sets the value by which this group will be sorted.
///
public IComparable SortValue {
get { return this.sortValue; }
set { this.sortValue = value; }
}
private IComparable sortValue;
///
/// Gets or sets the state of the group
///
public GroupState State {
get { return this.state; }
set { this.state = value; }
}
private GroupState state;
///
/// Gets or sets which bits of State are valid
///
public GroupState StateMask {
get { return this.stateMask; }
set { this.stateMask = value; }
}
private GroupState stateMask;
///
/// Gets or sets whether this group is showing only a subset of its elements
///
///
/// As of WinSDK v7.0, this property officially does nothing.
///
public bool Subseted {
get { return this.GetOneState(GroupState.LVGS_SUBSETED); }
set { this.SetOneState(value, GroupState.LVGS_SUBSETED); }
}
///
/// Gets or sets the user-defined data attached to this group
///
public object Tag {
get { return this.tag; }
set { this.tag = value; }
}
private object tag;
///
/// Gets or sets the task of this group
///
/// This task is the clickable text that appears on the right margin
/// of the group header.
public string Task {
get { return this.task; }
set { this.task = value; }
}
private string task;
///
/// Gets or sets the int or string that will select the image to be shown against the title
///
public object TitleImage {
get { return this.titleImage; }
set { this.titleImage = value; }
}
private object titleImage;
///
/// Gets or sets the top description of the group
///
///
/// Descriptions only appear when group is centered and there is a title image
///
public string TopDescription {
get { return this.topDescription; }
set { this.topDescription = value; }
}
private string topDescription;
///
/// Gets or sets the number of items that are within this group.
///
/// This should only be used for virtual groups.
public int VirtualItemCount {
get { return this.virtualItemCount; }
set { this.virtualItemCount = value; }
}
private int virtualItemCount;
#endregion
#region Protected properties
///
/// Gets or sets the ListViewGroup that is shadowed by this group.
///
/// For virtual groups, this will always be null.
protected ListViewGroup ListViewGroup {
get { return this.listViewGroup; }
set { this.listViewGroup = value; }
}
private ListViewGroup listViewGroup;
#endregion
#region Calculations/Conversions
///
/// Calculate the index into the group image list of the given image selector
///
///
///
public int GetImageIndex(object imageSelector) {
if (imageSelector == null || this.ListView == null || this.ListView.GroupImageList == null)
return -1;
if (imageSelector is Int32)
return (int)imageSelector;
String imageSelectorAsString = imageSelector as String;
if (imageSelectorAsString != null)
return this.ListView.GroupImageList.Images.IndexOfKey(imageSelectorAsString);
return -1;
}
///
/// Convert this object to a string representation
///
///
public override string ToString() {
return this.Header;
}
#endregion
#region Commands
///
/// Insert a native group into the underlying Windows control,
/// *without* using a ListViewGroup
///
///
/// This is used when creating virtual groups
public void InsertGroupNewStyle(ObjectListView olv) {
this.ListView = olv;
NativeMethods.InsertGroup(olv, this.AsNativeGroup(true));
}
///
/// Insert a native group into the underlying control via a ListViewGroup
///
///
public void InsertGroupOldStyle(ObjectListView olv) {
this.ListView = olv;
// Create/update the associated ListViewGroup
if (this.ListViewGroup == null)
this.ListViewGroup = new ListViewGroup();
this.ListViewGroup.Header = this.Header;
this.ListViewGroup.HeaderAlignment = this.HeaderAlignment;
this.ListViewGroup.Name = this.Name;
// Remember which OLVGroup created the ListViewGroup
this.ListViewGroup.Tag = this;
// Add the group to the control
olv.Groups.Add(this.ListViewGroup);
// Add any extra information
NativeMethods.SetGroupInfo(olv, this.GroupId, this.AsNativeGroup(false));
}
///
/// Change the members of the group to match the current contents of Items,
/// using a ListViewGroup
///
public void SetItemsOldStyle() {
List list = this.Items as List;
if (list == null) {
foreach (OLVListItem item in this.Items) {
this.ListViewGroup.Items.Add(item);
}
} else {
this.ListViewGroup.Items.AddRange(list.ToArray());
}
}
#endregion
#region Implementation
///
/// Create a native LVGROUP structure that matches this group
///
internal NativeMethods.LVGROUP2 AsNativeGroup(bool withId) {
NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2();
group.cbSize = (uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2));
group.mask = (uint)(GroupMask.LVGF_HEADER ^ GroupMask.LVGF_ALIGN ^ GroupMask.LVGF_STATE);
group.pszHeader = this.Header;
group.uAlign = (uint)this.HeaderAlignment;
group.stateMask = (uint)this.StateMask;
group.state = (uint)this.State;
if (withId) {
group.iGroupId = this.GroupId;
group.mask ^= (uint)GroupMask.LVGF_GROUPID;
}
if (!String.IsNullOrEmpty(this.Footer)) {
group.pszFooter = this.Footer;
group.mask ^= (uint)GroupMask.LVGF_FOOTER;
}
if (!String.IsNullOrEmpty(this.Subtitle)) {
group.pszSubtitle = this.Subtitle;
group.mask ^= (uint)GroupMask.LVGF_SUBTITLE;
}
if (!String.IsNullOrEmpty(this.Task)) {
group.pszTask = this.Task;
group.mask ^= (uint)GroupMask.LVGF_TASK;
}
if (!String.IsNullOrEmpty(this.TopDescription)) {
group.pszDescriptionTop = this.TopDescription;
group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONTOP;
}
if (!String.IsNullOrEmpty(this.BottomDescription)) {
group.pszDescriptionBottom = this.BottomDescription;
group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONBOTTOM;
}
int imageIndex = this.GetImageIndex(this.TitleImage);
if (imageIndex >= 0) {
group.iTitleImage = imageIndex;
group.mask ^= (uint)GroupMask.LVGF_TITLEIMAGE;
}
imageIndex = this.GetImageIndex(this.ExtendedImage);
if (imageIndex >= 0) {
group.iExtendedImage = imageIndex;
group.mask ^= (uint)GroupMask.LVGF_EXTENDEDIMAGE;
}
if (!String.IsNullOrEmpty(this.SubsetTitle)) {
group.pszSubsetTitle = this.SubsetTitle;
group.mask ^= (uint)GroupMask.LVGF_SUBSET;
}
if (this.VirtualItemCount > 0) {
group.cItems = this.VirtualItemCount;
group.mask ^= (uint)GroupMask.LVGF_ITEMS;
}
return group;
}
private bool GetOneState(GroupState mask) {
if (this.Created)
this.State = this.GetState();
return (this.State & mask) == mask;
}
///
/// Get the current state of this group from the underlying control
///
protected GroupState GetState() {
return NativeMethods.GetGroupState(this.ListView, this.GroupId, GroupState.LVGS_ALL);
}
///
/// Get the current state of this group from the underlying control
///
protected int SetState(GroupState newState, GroupState mask) {
NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2();
group.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2)));
group.mask = (uint)GroupMask.LVGF_STATE;
group.state = (uint)newState;
group.stateMask = (uint)mask;
return NativeMethods.SetGroupInfo(this.ListView, this.GroupId, group);
}
private void SetOneState(bool value, GroupState mask)
{
this.StateMask ^= mask;
if (value)
this.State ^= mask;
else
this.State &= ~mask;
if (this.Created)
this.SetState(this.State, mask);
}
#endregion
}
}