/* * ObjectListView - A listview to show various aspects of a collection of objects * * Author: Phillip Piper * Date: 9/10/2006 11:15 AM * * Change log * v2.9.1 * 2015-12-30 JPP - Added CellRendererGetter to allow each cell to have a different renderer. * - Obsolete properties are no longer code-gen'ed. * * v2.9.0 * 2015-08-22 JPP - Allow selected row back/fore colours to be specified for each row * - Renamed properties related to selection colours: * - HighlightBackgroundColor -> SelectedBackColor * - HighlightForegroundColor -> SelectedForeColor * - UnfocusedHighlightBackgroundColor -> UnfocusedSelectedBackColor * - UnfocusedHighlightForegroundColor -> UnfocusedSelectedForeColor * - UseCustomSelectionColors is no longer used * 2015-08-03 JPP - Added ObjectListView.CellEditFinished event * - Added EditorRegistry.Unregister() * 2015-07-08 JPP - All ObjectListViews are now OwnerDrawn by default. This allows all the great features * of ObjectListView to work correctly at the slight cost of more processing at render time. * It also avoids the annoying "hot item background ignored in column 0" behaviour that native * ListView has. Programmers can still turn it back off if they wish. * 2015-06-27 JPP - Yet another attempt to disable ListView's "shift click toggles checkboxes" behaviour. * The last strategy (fake right click) worked, but had nasty side effects. This one works * by intercepting a HITTEST message so that it fails. It no longer creates fake right mouse events. * - Trigger SelectionChanged when filter is changed * 2015-06-23 JPP - [BIG] Added support for Buttons * 2015-06-22 JPP - Added OLVColumn.SearchValueGetter to allow the text used when text filtering to be customised * - The default DefaultRenderer is now a HighlightTextRenderer, since that seems more generally useful * 2015-06-17 JPP - Added FocusedObject property * - Hot item is now always applied to the row even if FullRowSelect is false * 2015-06-11 JPP - Added DefaultHotItemStyle property * 2015-06-07 JPP - Added HeaderMinimumHeight property * - Added ObjectListView.CellEditUsesWholeCell and OLVColumn.CellEditUsesWholeCell properties. * 2015-05-15 JPP - Allow ImageGetter to return an Image (which I can't believe didn't work from the beginning!) * 2015-04-27 JPP - Fix bug where setting View to LargeIcon in the designer was not persisted * 2015-04-07 JPP - Ensure changes to row.Font in FormatRow are not wiped out by FormatCell (SF #141) * * v2.8.1 * 2014-10-15 JPP - Added CellEditActivateMode.SingleClickAlways mode * - Fire Filter event event if ModelFilter and ListFilter are null (SF #126) * - Fixed issue where single-click editing didn't work (SF #128) * v2.8.0 * 2014-10-11 JPP - Fixed some XP-only flicker issues * 2014-09-26 JPP - Fixed intricate bug involving checkboxes on non-owner-drawn virtual lists. * - Fixed long standing (but previously unreported) error on non-details virtual lists where * users could not click on checkboxes. * 2014-09-07 JPP - (Major) Added ability to have checkboxes in headers * - CellOver events are raised when the mouse moves over the header. Set TriggerCellOverEventsWhenOverHeader * to false to disable this behaviour. * - Freeze/Unfreeze now use BeginUpdate/EndUpdate to disable Window level drawing * - Changed default value of ObjectListView.HeaderUsesThemes from true to false. Too many people were * being confused, trying to make something interesting appear in the header and nothing showing up * 2014-08-04 JPP - Final attempt to fix the multiple hyperlink events being raised. This involves turning * a NM_CLICK notification into a NM_RCLICK. * 2014-05-21 JPP - (Major) Added ability to disable rows. DisabledObjects, DisableObjects(), DisabledItemStyle. * 2014-04-25 JPP - Fixed issue where virtual lists containing a single row didn't update hyperlinks on MouseOver * - Added sanity check before BuildGroups() * 2014-03-22 JPP - Fixed some subtle bugs resulting from misuse of TryGetValue() * 2014-03-09 JPP - Added CollapsedGroups property * - Several minor Resharper complaints quiesced. * v2.7 * 2014-02-14 JPP - Fixed issue with ShowHeaderInAllViews (another one!) where setting it to false caused the list to lose * its other extended styles, leading to nasty flickering and worse. * 2014-02-06 JPP - Fix issue on virtual lists where the filter was not correctly reapplied after columns were added or removed. * - Made disposing of cell editors optional (defaults to true). This allows controls to be cached and reused. * - Bracketed column resizing with BeginUpdate/EndUpdate to smooth redraws (thanks to Davide) * 2014-02-01 JPP - Added static property ObjectListView.GroupTitleDefault to allow the default group title to be localised. * 2013-09-24 JPP - Fixed issue in RefreshObjects() when model objects overrode the Equals()/GetHashCode() methods. * - Made sure get state checker were used when they should have been * 2013-04-21 JPP - Clicking on a non-groupable column header when showing groups will now sort * the group contents by that column. * v2.6 * 2012-08-16 JPP - Added ObjectListView.EditModel() -- a convenience method to start an edit operation on a model * 2012-08-10 JPP - Don't trigger selection changed events during sorting/grouping or add/removing columns * 2012-08-06 JPP - Don't start a cell edit operation when the user clicks on the background of a checkbox cell. * - Honor values from the BeforeSorting event when calling a CustomSorter * 2012-08-02 JPP - Added CellVerticalAlignment and CellPadding properties. * 2012-07-04 JPP - Fixed issue with cell editing where the cell editing didn't finish until the first idle event. * This meant that if you clicked and held on the scroll thumb to finish a cell edit, the editor * wouldn't be removed until the mouse was released. * 2012-07-03 JPP - Fixed issue with SingleClick cell edit mode where the cell editing would not begin until the * mouse moved after the click. * 2012-06-25 JPP - Fixed bug where removing a column from a LargeIcon or SmallIcon view would crash the control. * 2012-06-15 JPP - Added Reset() method, which definitively removes all rows *and* columns from an ObjectListView. * 2012-06-11 JPP - Added FilteredObjects property which returns the collection of objects that survives any installed filters. * 2012-06-04 JPP - [Big] Added UseNotifyPropertyChanged to allow OLV to listen for INotifyPropertyChanged events on models. * 2012-05-30 JPP - Added static property ObjectListView.IgnoreMissingAspects. If this is set to true, all * ObjectListViews will silently ignore missing aspect errors. Read the remarks to see why this would be useful. * 2012-05-23 JPP - Setting UseFilterIndicator to true now sets HeaderUsesTheme to false. * Also, changed default value of UseFilterIndicator to false. Previously, HeaderUsesTheme and UseFilterIndicator * defaulted to true, which was pointless since when the HeaderUsesTheme is true, UseFilterIndicator does nothing. * v2.5.1 * 2012-05-06 JPP - Fix bug where collapsing the first group would cause decorations to stop being drawn (SR #3502608) * 2012-04-23 JPP - Trigger GroupExpandingCollapsing event to allow the expand/collapse to be cancelled * - Fixed SetGroupSpacing() so it corrects updates the space between all groups. * - ResizeLastGroup() now does nothing since it was broken and I can't remember what it was * even supposed to do :) * 2012-04-18 JPP - Upgraded hit testing to include hits on groups. * - HotItemChanged is now correctly recalculated on each mouse move. Includes "hot" group information. * 2012-04-14 JPP - Added GroupStateChanged event. Useful for knowing when a group is collapsed/expanded. * - Added AdditionalFilter property. This filter is combined with the Excel-like filtering that * the end user might enact at runtime. * 2012-04-10 JPP - Added PersistentCheckBoxes property to allow primary checkboxes to remember their values * across list rebuilds. * 2012-04-05 JPP - Reverted some code to .NET 2.0 standard. * - Tweaked some code * 2012-02-05 JPP - Fixed bug when selecting a separator on a drop down menu * 2011-06-24 JPP - Added CanUseApplicationIdle property to cover cases where Application.Idle events * are not triggered. For example, when used within VS (and probably Office) extensions * Application.Idle is never triggered. Set CanUseApplicationIdle to false to handle * these cases. * - Handle cases where a second tool tip is installed onto the ObjectListView. * - Correctly recolour rows after an Insert or Move * - Removed m.LParam cast which could cause overflow issues on Win7/64 bit. * v2.5.0 * 2011-05-31 JPP - SelectObject() and SelectObjects() no longer deselect all other rows. Set the SelectedObject or SelectedObjects property to do that. * - Added CheckedObjectsEnumerable * - Made setting CheckedObjects more efficient on large collections * - Deprecated GetSelectedObject() and GetSelectedObjects() * 2011-04-25 JPP - Added SubItemChecking event * - Fixed bug in handling of NewValue on CellEditFinishing event * 2011-04-12 JPP - Added UseFilterIndicator * - Added some more localizable messages * 2011-04-10 JPP - FormatCellEventArgs now has a CellValue property, which is the model value displayed * by the cell. For example, for the Birthday column, the CellValue might be * DateTime(1980, 12, 31), whereas the cell's text might be 'Dec 31, 1980'. * 2011-04-04 JPP - Tweaked UseTranslucentSelection and UseTranslucentHotItem to look (a little) more * like Vista/Win7. * - Alternate colours are now only applied in Details view (as they always should have been) * - Alternate colours are now correctly recalculated after removing objects * 2011-03-29 JPP - Added SelectColumnsOnRightClickBehaviour to allow the selecting of columns mechanism * to be changed. Can now be InlineMenu (the default), SubMenu, or ModelDialog. * - ColumnSelectionForm was moved from the demo into the ObjectListView project itself. * - Ctrl-C copying is now able to use the DragSource to create the data transfer object. * 2011-03-19 JPP - All model object comparisons now use Equals rather than == (thanks to vulkanino) * - [Small Break] GetNextItem() and GetPreviousItem() now accept and return OLVListView * rather than ListViewItems. * 2011-03-07 JPP - [Big] Added Excel-style filtering. Right click on a header to show a Filtering menu. * - Added CellEditKeyEngine to allow key handling when cell editing to be completely customised. * Add CellEditTabChangesRows and CellEditEnterChangesRows to show some of these abilities. * 2011-03-06 JPP - Added OLVColumn.AutoCompleteEditorMode in preference to AutoCompleteEditor * (which is now just a wrapper). Thanks to Clive Haskins * - Added lots of docs to new classes * 2011-02-25 JPP - Preserve word wrap settings on TreeListView * - Resize last group to keep it on screen (thanks to ?) * 2010-11-16 JPP - Fixed (once and for all) DisplayIndex problem with Generator * - Changed the serializer used in SaveState()/RestoreState() so that it resolves on * class name alone. * - Fixed bug in GroupWithItemCountSingularFormatOrDefault * - Fixed strange flickering in grouped, owner drawn OLV's using RefreshObject() * v2.4.1 * 2010-08-25 JPP - Fixed bug where setting OLVColumn.CheckBoxes to false gave it a renderer * specialized for checkboxes. Oddly, this made Generator created owner drawn * lists appear to be completely empty. * - In IDE, all ObjectListView properties are now in a single "ObjectListView" category, * rather than splitting them between "Appearance" and "Behavior" categories. * - Added GroupingParameters.GroupComparer to allow groups to be sorted in a customizable fashion. * - Sorting of items within a group can be disabled by setting * GroupingParameters.PrimarySortOrder to None. * 2010-08-24 JPP - Added OLVColumn.IsHeaderVertical to make a column draw its header vertical. * - Added OLVColumn.HeaderTextAlign to control the alignment of a column's header text. * - Added HeaderMaximumHeight to limit how tall the header section can become * 2010-08-18 JPP - Fixed long standing bug where having 0 columns caused a InvalidCast exception. * - Added IncludeAllColumnsInDataObject property * - Improved BuildList(bool) so that it preserves scroll position even when * the listview is grouped. * 2010-08-08 JPP - Added OLVColumn.HeaderImageKey to allow column headers to have an image. * - CellEdit validation and finish events now have NewValue property. * 2010-08-03 JPP - Subitem checkboxes improvements: obey IsEditable, can be hot, can be disabled. * - No more flickering of selection when tabbing between cells * - Added EditingCellBorderDecoration to make it clearer which cell is being edited. * 2010-08-01 JPP - Added ObjectListView.SmoothingMode to control the smoothing of all graphics * operations * - Columns now cache their group item format strings so that they still work as * grouping columns after they have been removed from the listview. This cached * value is only used when the column is not part of the listview. * 2010-07-25 JPP - Correctly trigger a Click event when the mouse is clicked. * 2010-07-16 JPP - Invalidate the control before and after cell editing to make sure it looks right * 2010-06-23 JPP - Right mouse clicks on checkboxes no longer confuse them * 2010-06-21 JPP - Avoid bug in underlying ListView control where virtual lists in SmallIcon view * generate GETTOOLINFO msgs with invalid item indices. * - Fixed bug where FastObjectListView would throw an exception when showing hyperlinks * in any view except Details. * 2010-06-15 JPP - Fixed bug in ChangeToFilteredColumns() that resulted in column display order * being lost when a column was hidden. * - Renamed IsVista property to IsVistaOrLater which more accurately describes its function. * v2.4 * 2010-04-14 JPP - Prevent object disposed errors when mouse event handlers cause the * ObjectListView to be destroyed (e.g. closing a form during a * double click event). * - Avoid checkbox munging bug in standard ListView when shift clicking on non-primary * columns when FullRowSelect is true. * 2010-04-12 JPP - Fixed bug in group sorting (thanks Mike). * 2010-04-07 JPP - Prevent hyperlink processing from triggering spurious MouseUp events. * This showed itself by launching the same url multiple times. * 2010-04-06 JPP - Space filling columns correctly resize upon initial display * - ShowHeaderInAllViews is better but still not working reliably. * See comments on property for more details. * 2010-03-23 JPP - Added ObjectListView.HeaderFormatStyle and OLVColumn.HeaderFormatStyle. * This makes HeaderFont and HeaderForeColor properties unnecessary -- * they will be marked obsolete in the next version and removed after that. * 2010-03-16 JPP - Changed object checking so that objects can be pre-checked before they * are added to the list. Normal ObjectListViews managed "checkedness" in * the ListViewItem, so this won't work for them, unless check state getters * and putters have been installed. It will work on on virtual lists (thus fast lists and * tree views) since they manage their own check state. * 2010-03-06 JPP - Hide "Items" and "Groups" from the IDE properties grid since they shouldn't be set like that. * They can still be accessed through "Custom Commands" and there's nothing we can do * about that. * 2010-03-05 JPP - Added filtering * 2010-01-18 JPP - Overlays can be turned off. They also only work on 32-bit displays * v2.3 * 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics() * 2009-10-28 JPP - Fix bug when right clicking in the empty area of the header * 2009-10-20 JPP - Redraw the control after setting EmptyListMsg property * v2.3 * 2009-09-30 JPP - Added Dispose() method to properly release resources * 2009-09-16 JPP - Added OwnerDrawnHeader, which you can set to true if you want to owner draw * the header yourself. * 2009-09-15 JPP - Added UseExplorerTheme, which allow complete visual compliance with Vista explorer. * But see property documentation for its many limitations. * - Added ShowHeaderInAllViews. To make this work, Columns are no longer * changed when switching to/from Tile view. * 2009-09-11 JPP - Added OLVColumn.AutoCompleteEditor to allow the autocomplete of cell editors * to be disabled. * 2009-09-01 JPP - Added ObjectListView.TextRenderingHint property which controls the * text rendering hint of all drawn text. * 2009-08-28 JPP - [BIG] Added group formatting to supercharge what is possible with groups * - [BIG] Virtual groups now work * - Extended MakeGroupies() to handle more aspects of group creation * 2009-08-19 JPP - Added ability to show basic column commands when header is right clicked * - Added SelectedRowDecoration, UseTranslucentSelection and UseTranslucentHotItem. * - Added PrimarySortColumn and PrimarySortOrder * 2009-08-15 JPP - Correct problems with standard hit test and subitems * 2009-08-14 JPP - [BIG] Support Decorations * - [BIG] Added header formatting capabilities: font, color, word wrap * - Gave ObjectListView its own designer to hide unwanted properties * - Separated design time stuff into separate file * - Added FormatRow and FormatCell events * 2009-08-09 JPP - Get around bug in HitTest when not FullRowSelect * - Added OLVListItem.GetSubItemBounds() method which works correctly * for all columns including column 0 * 2009-08-07 JPP - Added Hot* properties that track where the mouse is * - Added HotItemChanged event * - Overrode TextAlign on columns so that column 0 can have something other * than just left alignment. This is only honored when owner drawn. * v2.2.1 * 2009-08-03 JPP - Subitem edit rectangles always allowed for an image in the cell, even if there was none. * Now they only allow for an image when there actually is one. * - Added Bounds property to OLVListItem which handles items being part of collapsed groups. * 2009-07-29 JPP - Added GetSubItem() methods to ObjectListView and OLVListItem * 2009-07-26 JPP - Avoided bug in .NET framework involving column 0 of owner drawn listviews not being * redrawn when the listview was scrolled horizontally (this was a LOT of work to track * down and fix!) * - The cell edit rectangle is now correctly calculated when the listview is scrolled * horizontally. * 2009-07-14 JPP - If the user clicks/double clicks on a tree list cell, an edit operation will no longer begin * if the click was to the left of the expander. This is implemented in such a way that * other renderers can have similar "dead" zones. * 2009-07-11 JPP - CalculateCellBounds() messed with the FullRowSelect property, which confused the * tooltip handling on the underlying control. It no longer does this. * - The cell edit rectangle is now correctly calculated for owner-drawn, non-Details views. * 2009-07-08 JPP - Added Cell events (CellClicked, CellOver, CellRightClicked) * - Made BuildList(), AddObject() and RemoveObject() thread-safe * 2009-07-04 JPP - Space bar now properly toggles checkedness of selected rows * 2009-07-02 JPP - Fixed bug with tooltips when the underlying Windows control was destroyed. * - CellToolTipShowing events are now triggered in all views. * v2.2 * 2009-06-02 JPP - BeforeSortingEventArgs now has a Handled property to let event handlers do * the item sorting themselves. * - AlwaysGroupByColumn works again, as does SortGroupItemsByPrimaryColumn and all their * various permutations. * - SecondarySortOrder and SecondarySortColumn are now "null" by default * 2009-05-15 JPP - Fixed bug so that KeyPress events are again triggered * 2009-05-10 JPP - Removed all unsafe code * 2009-05-07 JPP - Don't use glass panel for overlays when in design mode. It's too confusing. * 2009-05-05 JPP - Added Scroll event (thanks to Christophe Hosten for the complete patch to implement this) * - Added Unfocused foreground and background colors (also thanks to Christophe Hosten) * 2009-04-29 JPP - Added SelectedColumn property, which puts a slight tint on that column. Combine * this with TintSortColumn property and the sort column is automatically tinted. * - Use an overlay to implement "empty list" msg. Default empty list msg is now prettier. * 2009-04-28 JPP - Fixed bug where DoubleClick events were not triggered when CheckBoxes was true * 2009-04-23 JPP - Fixed various bugs under Vista. * - Made groups collapsible - Vista only. Thanks to Crustyapplesniffer. * - Forward events from DropSink to the control itself. This allows handlers to be defined * within the IDE for drop events * 2009-04-16 JPP - Made several properties localizable. * 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard * 2009-04-11 JPP - Implemented overlay architecture, based on CustomDraw scheme. * This unified drag drop feedback, empty list msgs and overlay images. * - Added OverlayImage and friends, which allows an image to be drawn * transparently over the listview * 2009-04-10 JPP - Fixed long-standing annoying flicker on owner drawn virtual lists! * This means, amongst other things, that grid lines no longer get confused, * and drag-select no longer flickers. * 2009-04-07 JPP - Calculate edit rectangles more accurately * 2009-04-06 JPP - Double-clicking no longer toggles the checkbox * - Double-clicking on a checkbox no longer confuses the checkbox * 2009-03-16 JPP - Optimized the build of autocomplete lists * v2.1 * 2009-02-24 JPP - Fix bug where double-clicking VERY quickly on two different cells * could give two editors * - Maintain focused item when rebuilding list (SF #2547060) * 2009-02-22 JPP - Reworked checkboxes so that events are triggered for virtual lists * 2009-02-15 JPP - Added ObjectListView.ConfigureAutoComplete utility method * 2009-02-02 JPP - Fixed bug with AlwaysGroupByColumn where column header clicks would not resort groups. * 2009-02-01 JPP - OLVColumn.CheckBoxes and TriStateCheckBoxes now work. * 2009-01-28 JPP - Complete overhaul of renderers! * - Use IRenderer * - Added ObjectListView.ItemRenderer to draw whole items * 2009-01-23 JPP - Simple Checkboxes now work properly * - Added TriStateCheckBoxes property to control whether the user can * set the row checkbox to have the Indeterminate value * - CheckState property is now just a wrapper around the StateImageIndex property * 2009-01-20 JPP - Changed to always draw columns when owner drawn, rather than falling back on DrawDefault. * This simplified several owner drawn problems * - Added DefaultRenderer property to help with the above * - HotItem background color is applied to all cells even when FullRowSelect is false * - Allow grouping by CheckedAspectName columns * - Commented out experimental animations. Still needs work. * 2009-01-17 JPP - Added HotItemStyle and UseHotItem to highlight the row under the cursor * - Added UseCustomSelectionColors property * - Owner draw mode now honors ForeColor and BackColor settings on the list * 2009-01-16 JPP - Changed to use EditorRegistry rather than hard coding cell editors * 2009-01-10 JPP - Changed to use Equals() method rather than == to compare model objects. * v2.0.1 * 2009-01-08 JPP - Fixed long-standing "multiple columns generated" problem. * Thanks to pinkjones for his help with solving this one! * - Added EnsureGroupVisible() * 2009-01-07 JPP - Made all public and protected methods virtual * - FinishCellEditing, PossibleFinishCellEditing and CancelCellEditing are now public * 2008-12-20 JPP - Fixed bug with group comparisons when a group key was null (SF#2445761) * 2008-12-19 JPP - Fixed bug with space filling columns and layout events * - Fixed RowHeight so that it only changes the row height, not the width of the images. * v2.0 * 2008-12-10 JPP - Handle Backspace key. Resets the search-by-typing state without delay * - Made some changes to the column collection editor to try and avoid * the multiple column generation problem. * - Updated some documentation * 2008-12-07 JPP - Search-by-typing now works when showing groups * - Added BeforeSearching and AfterSearching events which are triggered when the user types * into the list. * - Added secondary sort information to Before/AfterSorting events * - Reorganized group sorting code. Now triggers Sorting events. * - Added GetItemIndexInDisplayOrder() * - Tweaked in the interaction of the column editor with the IDE so that we (normally) * don't rely on a hack to find the owning ObjectListView * - Changed all 'DefaultValue(typeof(Color), "Empty")' to 'DefaultValue(typeof(Color), "")' * since the first does not given Color.Empty as I thought, but the second does. * 2008-11-28 JPP - Fixed long standing bug with horizontal scrollbar when shrinking the window. * (thanks to Bartosz Borowik) * 2008-11-25 JPP - Added support for dynamic tooltips * - Split out comparers and header controls stuff into their own files * 2008-11-21 JPP - Fixed bug where enabling grouping when there was not a sort column would not * produce a grouped list. Grouping column now defaults to column 0. * - Preserve selection on virtual lists when sorting * 2008-11-20 JPP - Added ability to search by sort column to ObjectListView. Unified this with * ability that was already in VirtualObjectListView * 2008-11-19 JPP - Fixed bug in ChangeToFilteredColumns() where DisplayOrder was not always restored correctly. * 2008-10-29 JPP - Event argument blocks moved to directly within the namespace, rather than being * nested inside ObjectListView class. * - Removed OLVColumn.CellEditor since it was never used. * - Marked OLVColumn.AspectGetterAutoGenerated as obsolete (it has not been used for * several versions now). * 2008-10-28 JPP - SelectedObjects is now an IList, rather than an ArrayList. This allows * it to accept generic list (eg List). * 2008-10-09 JPP - Support indeterminate checkbox values. * [BREAKING CHANGE] CheckStateGetter/CheckStatePutter now use CheckState types only. * BooleanCheckStateGetter and BooleanCheckStatePutter added to ease transition. * 2008-10-08 JPP - Added setFocus parameter to SelectObject(), which allows focus to be set * at the same time as selecting. * 2008-09-27 JPP - BIG CHANGE: Fissioned this file into separate files for each component * 2008-09-24 JPP - Corrected bug with owner drawn lists where a column 0 with a renderer * would draw at column 0 even if column 0 was dragged to another position. * - Correctly handle space filling columns when columns are added/removed * 2008-09-16 JPP - Consistently use try..finally for BeginUpdate()/EndUpdate() pairs * 2008-08-24 JPP - If LastSortOrder is None when adding objects, don't force a resort. * 2008-08-22 JPP - Catch and ignore some problems with setting TopIndex on FastObjectListViews. * 2008-08-05 JPP - In the right-click column select menu, columns are now sorted by display order, rather than alphabetically * v1.13 * 2008-07-23 JPP - Consistently use copy-on-write semantics with Add/RemoveObject methods * 2008-07-10 JPP - Enable validation on cell editors through a CellEditValidating event. * (thanks to Artiom Chilaru for the initial suggestion and implementation). * 2008-07-09 JPP - Added HeaderControl.Handle to allow OLV to be used within UserControls. * (thanks to Michael Coffey for tracking this down). * 2008-06-23 JPP - Split the more generally useful CopyObjectsToClipboard() method * out of CopySelectionToClipboard() * 2008-06-22 JPP - Added AlwaysGroupByColumn and AlwaysGroupBySortOrder, which * force the list view to always be grouped by a particular column. * 2008-05-31 JPP - Allow check boxes on FastObjectListViews * - Added CheckedObject and CheckedObjects properties * 2008-05-11 JPP - Allow selection foreground and background colors to be changed. * Windows doesn't allow this, so we can only make it happen when owner * drawing. Set the HighlightForegroundColor and HighlightBackgroundColor * properties and then call EnableCustomSelectionColors(). * v1.12 * 2008-05-08 JPP - Fixed bug where the column select menu would not appear if the * ObjectListView has a context menu installed. * 2008-05-05 JPP - Non detail views can now be owner drawn. The renderer installed for * primary column is given the chance to render the whole item. * See BusinessCardRenderer in the demo for an example. * - BREAKING CHANGE: RenderDelegate now returns a bool to indicate if default * rendering should be done. Previously returned void. Only important if your * code used RendererDelegate directly. Renderers derived from BaseRenderer * are unchanged. * 2008-05-03 JPP - Changed cell editing to use values directly when the values are Strings. * Previously, values were always handed to the AspectToStringConverter. * - When editing a cell, tabbing no longer tries to edit the next subitem * when not in details view! * 2008-05-02 JPP - MappedImageRenderer can now handle a Aspects that return a collection * of values. Each value will be drawn as its own image. * - Made AddObjects() and RemoveObjects() work for all flavours (or at least not crash) * - Fixed bug with clearing virtual lists that has been scrolled vertically * - Made TopItemIndex work with virtual lists. * 2008-05-01 JPP - Added AddObjects() and RemoveObjects() to allow faster mods to the list * - Reorganised public properties. Now alphabetical. * - Made the class ObjectListViewState internal, as it always should have been. * v1.11 * 2008-04-29 JPP - Preserve scroll position when building the list or changing columns. * - Added TopItemIndex property. Due to problems with the underlying control, this * property is not always reliable. See property docs for info. * 2008-04-27 JPP - Added SelectedIndex property. * - Use a different, more general strategy to handle Invoke(). Removed all delegates * that were only declared to support Invoke(). * - Check all native structures for 64-bit correctness. * 2008-04-25 JPP - Released on SourceForge. * 2008-04-13 JPP - Added ColumnRightClick event. * - Made the assembly CLS-compliant. To do this, our cell editors were made internal, and * the constraint on FlagRenderer template parameter was removed (the type must still * be an IConvertible, but if it isn't, the error will be caught at runtime, not compile time). * 2008-04-12 JPP - Changed HandleHeaderRightClick() to have a columnIndex parameter, which tells * exactly which column was right-clicked. * 2008-03-31 JPP - Added SaveState() and RestoreState() * - When cell editing, scrolling with a mouse wheel now ends the edit operation. * v1.10 * 2008-03-25 JPP - Added space filling columns. See OLVColumn.FreeSpaceProportion property for details. * A space filling columns fills all (or a portion) of the width unoccupied by other columns. * 2008-03-23 JPP - Finished tinkering with support for Mono. Compile with conditional compilation symbol 'MONO' * to enable. On Windows, current problems with Mono: * - grid lines on virtual lists crashes * - when grouped, items sometimes are not drawn when any item is scrolled out of view * - i can't seem to get owner drawing to work * - when editing cell values, the editing controls always appear behind the listview, * where they function fine -- the user just can't see them :-) * 2008-03-16 JPP - Added some methods suggested by Chris Marlowe (thanks for the suggestions Chris) * - ClearObjects() * - GetCheckedObject(), GetCheckedObjects() * - GetItemAt() variation that gets both the item and the column under a point * 2008-02-28 JPP - Fixed bug with subitem colors when using OwnerDrawn lists and a RowFormatter. * v1.9.1 * 2008-01-29 JPP - Fixed bug that caused owner-drawn virtual lists to use 100% CPU * - Added FlagRenderer to help draw bitwise-OR'ed flag values * 2008-01-23 JPP - Fixed bug (introduced in v1.9) that made alternate row colour with groups not quite right * - Ensure that DesignerSerializationVisibility.Hidden is set on all non-browsable properties * - Make sure that sort indicators are shown after changing which columns are visible * 2008-01-21 JPP - Added FastObjectListView * v1.9 * 2008-01-18 JPP - Added IncrementalUpdate() * 2008-01-16 JPP - Right clicking on column header will allow the user to choose which columns are visible. * Set SelectColumnsOnRightClick to false to prevent this behaviour. * - Added ImagesRenderer to draw more than one images in a column * - Changed the positioning of the empty list m to use all the client area. Thanks to Matze. * 2007-12-13 JPP - Added CopySelectionToClipboard(). Ctrl-C invokes this method. Supports text * and HTML formats. * 2007-12-12 JPP - Added support for checkboxes via CheckStateGetter and CheckStatePutter properties. * - Made ObjectListView and OLVColumn into partial classes so that others can extend them. * 2007-12-09 JPP - Added ability to have hidden columns, i.e. columns that the ObjectListView knows * about but that are not visible to the user. Controlled by OLVColumn.IsVisible. * Added ColumnSelectionForm to the project to show how it could be used in an application. * * v1.8 * 2007-11-26 JPP - Cell editing fully functional * 2007-11-21 JPP - Added SelectionChanged event. This event is triggered once when the * selection changes, no matter how many items are selected or deselected (in * contrast to SelectedIndexChanged which is called once for every row that * is selected or deselected). Thanks to lupokehl42 (Daniel) for his suggestions and * improvements on this idea. * 2007-11-19 JPP - First take at cell editing * 2007-11-17 JPP - Changed so that items within a group are not sorted if lastSortOrder == None * - Only call MakeSortIndicatorImages() if we haven't already made the sort indicators * (Corrected misspelling in the name of the method too) * 2007-11-06 JPP - Added ability to have secondary sort criteria when sorting * (SecondarySortColumn and SecondarySortOrder properties) * - Added SortGroupItemsByPrimaryColumn to allow group items to be sorted by the * primary column. Previous default was to sort by the grouping column. * v1.7 * No big changes to this version but made to work with ListViewPrinter and released with it. * * 2007-11-05 JPP - Changed BaseRenderer to use DrawString() rather than TextRenderer, since TextRenderer * does not work when printing. * v1.6 * 2007-11-03 JPP - Fixed some bugs in the rebuilding of DataListView. * 2007-10-31 JPP - Changed to use builtin sort indicators on XP and later. This also avoids alignment * problems on Vista. (thanks to gravybod for the suggestion and example implementation) * 2007-10-21 JPP - Added MinimumWidth and MaximumWidth properties to OLVColumn. * - Added ability for BuildList() to preserve selection. Calling BuildList() directly * tries to preserve selection; calling SetObjects() does not. * - Added SelectAll() and DeselectAll() methods. Useful for working with large lists. * 2007-10-08 JPP - Added GetNextItem() and GetPreviousItem(), which walk sequentially through the * listview items, even when the view is grouped. * - Added SelectedItem property * 2007-09-28 JPP - Optimized aspect-to-string conversion. BuildList() 15% faster. * - Added empty implementation of RefreshObjects() to VirtualObjectListView since * RefreshObjects() cannot work on virtual lists. * 2007-09-13 JPP - Corrected bug with custom sorter in VirtualObjectListView (thanks for mpgjunky) * 2007-09-07 JPP - Corrected image scaling bug in DrawAlignedImage() (thanks to krita970) * 2007-08-29 JPP - Allow item count labels on groups to be set per column (thanks to cmarlow for idea) * 2007-08-14 JPP - Major rework of DataListView based on Ian Griffiths's great work * 2007-08-11 JPP - When empty, the control can now draw a "List Empty" m * - Added GetColumn() and GetItem() methods * v1.5 * 2007-08-03 JPP - Support animated GIFs in ImageRenderer * - Allow height of rows to be specified - EXPERIMENTAL! * 2007-07-26 JPP - Optimised redrawing of owner-drawn lists by remembering the update rect * - Allow sort indicators to be turned off * 2007-06-30 JPP - Added RowFormatter delegate * - Allow a different label when there is only one item in a group (thanks to cmarlow) * v1.4 * 2007-04-12 JPP - Allow owner drawn on steriods! * - Column headers now display sort indicators * - ImageGetter delegates can now return ints, strings or Images * (Images are only visible if the list is owner drawn) * - Added OLVColumn.MakeGroupies to help with group partitioning * - All normal listview views are now supported * - Allow dotted aspect names, e.g. Owner.Workgroup.Name (thanks to OlafD) * - Added SelectedObject and SelectedObjects properties * v1.3 * 2007-03-01 JPP - Added DataListView * - Added VirtualObjectListView * - Added Freeze/Unfreeze capabilities * - Allowed sort handler to be installed * - Simplified sort comparisons: handles 95% of cases with only 6 lines of code! * - Fixed bug with alternative line colors on unsorted lists (thanks to cmarlow) * 2007-01-13 JPP - Fixed bug with lastSortOrder (thanks to Kwan Fu Sit) * - Non-OLVColumns are no longer allowed * 2007-01-04 JPP - Clear sorter before rebuilding list. 10x faster! (thanks to aaberg) * - Include GetField in GetAspectByName() so field values can be Invoked too. * - Fixed subtle bug in RefreshItem() that erased background colors. * 2006-11-01 JPP - Added alternate line colouring * 2006-10-20 JPP - Refactored all sorting comparisons and made it extendable. See ComparerManager. * - Improved IDE integration * - Made control DoubleBuffered * - Added object selection methods * 2006-10-13 JPP Implemented grouping and column sorting * 2006-10-09 JPP Initial version * * TO DO: * - Support undocumented group features: subseted groups, group footer items * * Copyright (C) 2006-2016 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@gmail.com. */ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; using System.Windows.Forms; using System.Windows.Forms.VisualStyles; using System.Runtime.Serialization.Formatters; using System.Threading; namespace BrightIdeasSoftware { /// /// An ObjectListView is a much easier to use, and much more powerful, version of the ListView. /// /// /// /// An ObjectListView automatically populates a ListView control with information taken /// from a given collection of objects. It can do this because each column is configured /// to know which bit of the model object (the "aspect") it should be displaying. Columns similarly /// understand how to sort the list based on their aspect, and how to construct groups /// using their aspect. /// /// /// Aspects are extracted by giving the name of a method to be called or a /// property to be fetched. These names can be simple names or they can be dotted /// to chain property access e.g. "Owner.Address.Postcode". /// Aspects can also be extracted by installing a delegate. /// /// /// An ObjectListView can show a "this list is empty" message when there is nothing to show in the list, /// so that the user knows the control is supposed to be empty. /// /// /// Right clicking on a column header should present a menu which can contain: /// commands (sort, group, ungroup); filtering; and column selection. Whether these /// parts of the menu appear is controlled by ShowCommandMenuOnRightClick, /// ShowFilterMenuOnRightClick and SelectColumnsOnRightClick respectively. /// /// /// The groups created by an ObjectListView can be configured to include other formatting /// information, including a group icon, subtitle and task button. Using some undocumented /// interfaces, these groups can even on virtual lists. /// /// /// ObjectListView supports dragging rows to other places, including other application. /// Special support is provide for drops from other ObjectListViews in the same application. /// In many cases, an ObjectListView becomes a full drag source by setting to /// true. Similarly, to accept drops, it is usually enough to set to true, /// and then handle the and events (or the and /// events, if you only want to handle drops from other ObjectListViews in your application). /// /// /// For these classes to build correctly, the project must have references to these assemblies: /// /// /// System /// System.Data /// System.Design /// System.Drawing /// System.Windows.Forms (obviously) /// /// [Designer(typeof(BrightIdeasSoftware.Design.ObjectListViewDesigner))] public partial class ObjectListView : ListView, ISupportInitialize { #region Life and death /// /// 创建一个 ObjectListView /// public ObjectListView() { this.ColumnClick += new ColumnClickEventHandler(this.HandleColumnClick); this.Layout += new LayoutEventHandler(this.HandleLayout); this.ColumnWidthChanging += new ColumnWidthChangingEventHandler(this.HandleColumnWidthChanging); this.ColumnWidthChanged += new ColumnWidthChangedEventHandler(this.HandleColumnWidthChanged); base.View = View.Details; // Turn on owner draw so that we are responsible for our own fates (and isolated from bugs in the underlying ListView) this.OwnerDraw = true; // ReSharper disable DoNotCallOverridableMethodsInConstructor this.DoubleBuffered = true; // kill nasty flickers. hiss... me hates 'em this.ShowSortIndicators = true; // Setup the overlays that will be controlled by the IDE settings this.InitializeStandardOverlays(); this.InitializeEmptyListMsgOverlay(); // ReSharper restore DoNotCallOverridableMethodsInConstructor } /// /// Dispose of any resources this instance has been using /// /// protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; foreach (GlassPanelForm glassPanel in this.glassPanels) { glassPanel.Unbind(); glassPanel.Dispose(); } this.glassPanels.Clear(); this.UnsubscribeNotifications(null); } #endregion // TODO //public CheckBoxSettings CheckBoxSettings { // get { return checkBoxSettings; } // private set { checkBoxSettings = value; } //} #region Static properties /// /// 获取鼠标左键是否在此时按下。 /// public static bool IsLeftMouseDown { get { return (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; } } /// /// 获取该程序是否在Vista或更高版本上运行 /// static public bool IsVistaOrLater { get { if (!ObjectListView.sIsVistaOrLater.HasValue) ObjectListView.sIsVistaOrLater = Environment.OSVersion.Version.Major >= 6; return ObjectListView.sIsVistaOrLater.Value; } } static private bool? sIsVistaOrLater; /// /// 获取该程序是否在Win7或更高版本上运行 /// static public bool IsWin7OrLater { get { if (!ObjectListView.sIsWin7OrLater.HasValue) { // For some reason, Win7 is v6.1, not v7.0 Version version = Environment.OSVersion.Version; ObjectListView.sIsWin7OrLater = version.Major > 6 || (version.Major == 6 && version.Minor > 0); } return ObjectListView.sIsWin7OrLater.Value; } } static private bool? sIsWin7OrLater; /// /// Gets or sets how what smoothing mode will be applied to graphic operations. /// static public System.Drawing.Drawing2D.SmoothingMode SmoothingMode { get { return ObjectListView.sSmoothingMode; } set { ObjectListView.sSmoothingMode = value; } } static private System.Drawing.Drawing2D.SmoothingMode sSmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; /// /// Gets or sets how should text be renderered. /// static public System.Drawing.Text.TextRenderingHint TextRenderingHint { get { return ObjectListView.sTextRendereringHint; } set { ObjectListView.sTextRendereringHint = value; } } static private System.Drawing.Text.TextRenderingHint sTextRendereringHint = System.Drawing.Text.TextRenderingHint.SystemDefault; /// /// 获取或设置当组Key为Null时将用于标题组的字符串。将其公开以使其可以本地化。 /// static public string GroupTitleDefault { get { return ObjectListView.sGroupTitleDefault; } set { ObjectListView.sGroupTitleDefault = value ?? "{无分组}"; } }static private string sGroupTitleDefault = "{无分组}"; /// /// Convert the given enumerable into an ArrayList as efficiently as possible /// /// The source collection /// If true, this method will always create a new /// collection. /// An ArrayList with the same contents as the given collection. /// /// When we move to .NET 3.5, we can use LINQ and not need this method. /// public static ArrayList EnumerableToArray(IEnumerable collection, bool alwaysCreate) { if (collection == null) return new ArrayList(); if (!alwaysCreate) { ArrayList array = collection as ArrayList; if (array != null) return array; IList iList = collection as IList; if (iList != null) return ArrayList.Adapter(iList); } ICollection iCollection = collection as ICollection; if (iCollection != null) return new ArrayList(iCollection); ArrayList newObjects = new ArrayList(); foreach (object x in collection) newObjects.Add(x); return newObjects; } /// /// Return the count of items in the given enumerable /// /// /// /// When we move to .NET 3.5, we can use LINQ and not need this method. public static int EnumerableCount(IEnumerable collection) { if (collection == null) return 0; ICollection iCollection = collection as ICollection; if (iCollection != null) return iCollection.Count; int i = 0; // ReSharper disable once UnusedVariable foreach (object x in collection) i++; return i; } /// /// Return whether or not the given enumerable is empty. A string is regarded as /// an empty collection. /// /// /// True if the given collection is null or empty /// /// When we move to .NET 3.5, we can use LINQ and not need this method. /// public static bool IsEnumerableEmpty(IEnumerable collection) { return collection == null || (collection is string) || !collection.GetEnumerator().MoveNext(); } /// /// Gets or sets whether all ObjectListViews will silently ignore missing aspect errors. /// /// /// /// By default, if an ObjectListView is asked to display an aspect /// (i.e. a field/property/method) /// that does not exist from a model, it displays an error message in that cell, since that /// condition is normally a programming error. There are some use cases where /// this is not an error -- in those cases, set this to true and ObjectListView will /// simply display an empty cell. /// /// Be warned: if you set this to true, it can be very difficult to track down /// typing mistakes or name changes in AspectNames. /// public static bool IgnoreMissingAspects { get { return Munger.IgnoreMissingAspects; } set { Munger.IgnoreMissingAspects = value; } } /// /// Gets or sets whether the control will draw a rectangle in each cell showing the cell padding. /// /// /// /// This can help with debugging display problems from cell padding. /// /// As with all cell padding, this setting only takes effect when the control is owner drawn. /// public static bool ShowCellPaddingBounds { get { return sShowCellPaddingBounds; } set { sShowCellPaddingBounds = value; } } private static bool sShowCellPaddingBounds; /// /// Gets the style that will be used by default to format disabled rows /// public static SimpleItemStyle DefaultDisabledItemStyle { get { if (sDefaultDisabledItemStyle == null) { sDefaultDisabledItemStyle = new SimpleItemStyle(); sDefaultDisabledItemStyle.ForeColor = Color.DarkGray; } return sDefaultDisabledItemStyle; } } private static SimpleItemStyle sDefaultDisabledItemStyle; /// /// Gets the style that will be used by default to format hot rows /// public static HotItemStyle DefaultHotItemStyle { get { if (sDefaultHotItemStyle == null) { sDefaultHotItemStyle = new HotItemStyle(); sDefaultHotItemStyle.BackColor = Color.FromArgb(224, 235, 253); } return sDefaultHotItemStyle; } } private static HotItemStyle sDefaultHotItemStyle; #endregion #region Public properties /// /// Gets or sets an model filter that is combined with any column filtering that the end-user specifies. /// /// This is different from the ModelFilter property, since setting that will replace /// any column filtering, whereas setting this will combine this filter with the column filtering [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IModelFilter AdditionalFilter { get { return this.additionalFilter; } set { if (this.additionalFilter == value) return; this.additionalFilter = value; this.UpdateColumnFiltering(); } } private IModelFilter additionalFilter; /// /// Get or set all the columns that this control knows about. /// Only those columns where IsVisible is true will be seen by the user. /// /// /// /// If you want to add new columns programmatically, add them to /// AllColumns and then call RebuildColumns(). Normally, you do not have to /// deal with this property directly. Just use the IDE. /// /// If you do add or remove columns from the AllColumns collection, /// you have to call RebuildColumns() to make those changes take effect. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public virtual List AllColumns { get { return this.allColumns; } set { this.allColumns = value ?? new List(); } } private List allColumns = new List(); /// /// 获取或设置每隔一行的背景色 /// [Category("ObjectListView"), Description("如果使用交替颜色,更改行的背景应该是什么颜色?"), DefaultValue(typeof(Color), "")] public Color AlternateRowBackColor { get { return alternateRowBackColor; } set { alternateRowBackColor = value; } } private Color alternateRowBackColor = Color.Empty; /// ///获取已设置的交替行背景色或默认颜色 /// [Browsable(false)] public virtual Color AlternateRowBackColorOrDefault { get { return this.alternateRowBackColor == Color.Empty ? Color.LemonChiffon : this.alternateRowBackColor; } } /// ///此属性强制ObjectListView始终按给定列对项进行分组。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OLVColumn AlwaysGroupByColumn { get { return alwaysGroupByColumn; } set { alwaysGroupByColumn = value; } } private OLVColumn alwaysGroupByColumn; /// ///如果AlwaysGroupByColumn不为空,则此属性将用于确定如何对这些组进行排序。 ///如果此属性的值为SortOrder.None,则排序顺序将根据用户上次单击的标题切换。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual SortOrder AlwaysGroupBySortOrder { get { return alwaysGroupBySortOrder; } set { alwaysGroupBySortOrder = value; } } private SortOrder alwaysGroupBySortOrder = SortOrder.None; /// /// Give access to the image list that is actually being used by the control /// /// /// Normally, it is preferable to use SmallImageList. Only use this property /// if you know exactly what you are doing. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual ImageList BaseSmallImageList { get { return base.SmallImageList; } set { base.SmallImageList = value; } } /// ///获取或设置编辑单元格的方式 /// /// Columns can also be marked as editable. [Category("ObjectListView"), Description("获取或设置编辑单元格的方式"), DefaultValue(CellEditActivateMode.None)] public virtual CellEditActivateMode CellEditActivation { get { return cellEditActivation; } set { cellEditActivation = value; if (this.Created) this.Invalidate(); } } private CellEditActivateMode cellEditActivation = CellEditActivateMode.None; /// ///编辑单元格时,是否应该使用整个单元格(减去复选框或图像使用的任何空间)? /// 默认值为 true. /// /// /// This is always treated as true when the control is NOT owner drawn. /// /// When this is false and the control is owner drawn, /// ObjectListView will try to calculate the width of the cell's /// actual contents, and then size the editing control to be just the right width. If this is true, /// the whole width of the cell will be used, regardless of the cell's contents. /// /// Each column can have a different value for property. This value from the control is only /// used when a column is not specified one way or another. /// Regardless of this setting, developers can specify the exact size of the editing control /// by listening for the CellEditStarting event. /// [Category("ObjectListView"), Description("编辑单元格时,是否应该使用整个单元格?"), DefaultValue(true)] public virtual bool CellEditUseWholeCell { get { return cellEditUseWholeCell; } set { cellEditUseWholeCell = value; } } private bool cellEditUseWholeCell; /// /// Gets or sets the engine that will handle key presses during a cell edit operation. /// Settings this to null will reset it to default value. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public CellEditKeyEngine CellEditKeyEngine { get { return this.cellEditKeyEngine ?? (this.cellEditKeyEngine = new CellEditKeyEngine()); } set { this.cellEditKeyEngine = value; } } private CellEditKeyEngine cellEditKeyEngine; /// /// 获取当前用于编辑单元格的控件。 /// /// This will obviously be null if no cell is being edited. [Browsable(false)] public Control CellEditor { get { return this.cellEditor; } } /// ///获取或设置编辑控件左边缘或右边缘的单元格时Tab键的行为。 ///如果为False(默认值),则按Tab键将换行到同一行的另一侧。 ///如果为True,编辑最右边的单元格时按Tab键将前进到下一行, ///编辑最左边的单元格时按Shift-Tab组合键将更改为上一行。 /// [Category("ObjectListView"), Description("单元格编辑时是否应使用Tab/Shift-Tab组合键更改行? "), DefaultValue(false)] public virtual bool CellEditTabChangesRows { get { return cellEditTabChangesRows; } set { cellEditTabChangesRows = value; if (cellEditTabChangesRows) { this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.ChangeRow); this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab|Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.ChangeRow); } else { this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.Wrap); this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab | Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.Wrap); } } } private bool cellEditTabChangesRows; /// /// 获取或设置编辑单元格时Enter键的行为。 /// 如果为False(默认值),则按Enter键将简单地完成编辑操作。 /// 如果为True,则Enter将完成编辑操作,并在当前单元格下方的单元格上启动新的编辑操作,在位于底部单元格时换行到下一行的顶部。 /// [Category("ObjectListView"), Description("获取或设置编辑单元格时Enter键的行为"), DefaultValue(false)] public virtual bool CellEditEnterChangesRows { get { return cellEditEnterChangesRows; } set { cellEditEnterChangesRows = value; if (cellEditEnterChangesRows) { this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.ChangeRowDown, CellEditAtEdgeBehaviour.ChangeColumn); this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.ChangeRowUp, CellEditAtEdgeBehaviour.ChangeColumn); } else { this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); } } } private bool cellEditEnterChangesRows; /// ///获取显示单元格提示的工具提示控件 /// [Browsable(false)] public ToolTipControl CellToolTip { get { if (this.cellToolTip == null) { this.CreateCellToolTip(); } return this.cellToolTip; } } private ToolTipControl cellToolTip; /// /// 获取或设置该项的每个单元格周围将留空的像素数。单元格内容在考虑填充后对齐。 /// /// /// Each value of the given rectangle will be treated as an inset from /// the corresponding side. The width of the rectangle is the padding for the /// right cell edge. The height of the rectangle is the padding for the bottom /// cell edge. /// /// /// So, this.olv1.CellPadding = new Rectangle(1, 2, 3, 4); will leave one pixel /// of space to the left of the cell, 2 pixels at the top, 3 pixels of space /// on the right edge, and 4 pixels of space at the bottom of each cell. /// /// /// This setting only takes effect when the control is owner drawn. /// /// This setting only affects the contents of the cell. The background is /// not affected. /// If you set this to a foolish value, your control will appear to be empty. /// [Category("ObjectListView"), Description("获取或设置该项的每个单元格周围将留空的像素数。单元格内容在考虑填充后对齐。"), DefaultValue(null)] public Rectangle? CellPadding { get { return this.cellPadding; } set { this.cellPadding = value; } } private Rectangle? cellPadding; /// ///获取或设置默认情况下单元格垂直对齐的方式。 /// /// This setting only takes effect when the control is owner drawn. It will only be noticable /// when RowHeight has been set such that there is some vertical space in each row. [Category("ObjectListView"), Description("获取或设置默认情况下单元格垂直对齐的方式。"), DefaultValue(StringAlignment.Center)] public virtual StringAlignment CellVerticalAlignment { get { return this.cellVerticalAlignment; } set { this.cellVerticalAlignment = value; } } private StringAlignment cellVerticalAlignment = StringAlignment.Center; /// /// Should this list show checkboxes? /// public new bool CheckBoxes { get { return base.CheckBoxes; } set { // Due to code in the base ListView class, turning off CheckBoxes on a virtual // list always throws an InvalidOperationException. We have to do some major hacking // to get around that if (this.VirtualMode) { // Leave virtual mode this.StateImageList = null; this.VirtualListSize = 0; this.VirtualMode = false; // Change the CheckBox setting while not in virtual mode base.CheckBoxes = value; // Reinstate virtual mode this.VirtualMode = true; // Re-enact the bits that we lost by switching to virtual mode this.ShowGroups = this.ShowGroups; this.BuildList(true); } else { base.CheckBoxes = value; // Initialize the state image list so we can display indetermined values. this.InitializeStateImageList(); } } } /// /// 返回选中行的模型对象;如果未选中行或选中多行,则返回NULL /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual Object CheckedObject { get { IList checkedObjects = this.CheckedObjects; return checkedObjects.Count == 1 ? checkedObjects[0] : null; } set { this.CheckedObjects = new ArrayList(new Object[] { value }); } } /// /// Get or set the collection of model objects that are checked. /// When setting this property, any row whose model object isn't /// in the given collection will be unchecked. Setting to null is /// equivilent to unchecking all. /// /// /// /// This property returns a simple collection. Changes made to the returned /// collection do NOT affect the list. This is different to the behaviour of /// CheckedIndicies collection. /// /// /// .NET's CheckedItems property is not helpful. It is just a short-hand for /// iterating through the list looking for items that are checked. /// /// /// The performance of the get method is O(n), where n is the number of items /// in the control. The performance of the set method is /// O(n + m) where m is the number of objects being checked. Be careful on long lists. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IList CheckedObjects { get { ArrayList list = new ArrayList(); if (this.CheckBoxes) { for (int i = 0; i < this.GetItemCount(); i++) { OLVListItem olvi = this.GetItem(i); if (olvi.CheckState == CheckState.Checked) list.Add(olvi.RowObject); } } return list; } set { if (!this.CheckBoxes) return; Stopwatch sw = Stopwatch.StartNew(); // Set up an efficient way of testing for the presence of a particular model Hashtable table = new Hashtable(this.GetItemCount()); if (value != null) { foreach (object x in value) table[x] = true; } this.BeginUpdate(); foreach (Object x in this.Objects) { this.SetObjectCheckedness(x, table.ContainsKey(x) ? CheckState.Checked : CheckState.Unchecked); } this.EndUpdate(); Debug.WriteLine(String.Format("PERF - Setting CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); } } /// ///获取或设置可枚举对象中的选中对象。 /// /// /// Useful for checking all objects in the list. /// /// /// this.olv1.CheckedObjectsEnumerable = this.olv1.Objects; /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IEnumerable CheckedObjectsEnumerable { get { return this.CheckedObjects; } set { this.CheckedObjects = ObjectListView.EnumerableToArray(value, true); } } /// /// Gets Columns for this list. We hide the original so we can associate /// a specialised editor with it. /// [Editor("BrightIdeasSoftware.Design.OLVColumnCollectionEditor", "System.Drawing.Design.UITypeEditor")] new public ListView.ColumnHeaderCollection Columns { get { return base.Columns; } } /// ///获取/设置当列表切换到TileView(平铺视图)时应使用的列的列表。 /// [Browsable(false), Obsolete("Use GetFilteredColumns() and OLVColumn.IsTileViewColumn instead"), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public List ColumnsForTileView { get { return this.GetFilteredColumns(View.Tile); } } /// ///按向用户显示的顺序返回可见列 /// [Browsable(false)] public virtual List ColumnsInDisplayOrder { get { OLVColumn[] columnsInDisplayOrder = new OLVColumn[this.Columns.Count]; foreach (OLVColumn col in this.Columns) { columnsInDisplayOrder[col.DisplayIndex] = col; } return new List(columnsInDisplayOrder); } } /// /// 获取显示列表的控件区域,减去任何标题控件 /// [Browsable(false)] public Rectangle ContentRectangle { get { Rectangle r = this.ClientRectangle; // If the listview has a header control, remove the header from the control area if ((this.View == View.Details || this.ShowHeaderInAllViews) && this.HeaderControl != null) { Rectangle hdrBounds = new Rectangle(); NativeMethods.GetClientRect(this.HeaderControl.Handle, ref hdrBounds); r.Y = hdrBounds.Height; r.Height = r.Height - hdrBounds.Height; } return r; } } /// ///获取或设置当用户按Ctrl+C组合键时是否应将选定行复制到剪贴板 /// [Category("ObjectListView"), Description("获取或设置当用户按Ctrl+C组合键时是否应将选定行复制到剪贴板"), DefaultValue(true)] public virtual bool CopySelectionOnControlC { get { return copySelectionOnControlC; } set { copySelectionOnControlC = value; } } private bool copySelectionOnControlC = true; /// /// 获取或设置按Ctrl+C键复制到剪贴板功能是否应使用安装的DragSource创建放置到剪贴板上的数据对象。 /// /// This is normally what is desired, unless a custom DragSource is installed /// that does some very specialized drag-drop behaviour. [Category("ObjectListView"), Description("获取或设置按Ctrl + C键复制到剪贴板功能是否应使用安装的DragSource创建放置到剪贴板上的数据对象。"), DefaultValue(true)] public bool CopySelectionOnControlCUsesDragSource { get { return this.copySelectionOnControlCUsesDragSource; } set { this.copySelectionOnControlCUsesDragSource = value; } } private bool copySelectionOnControlCUsesDragSource = true; /// /// Gets the list of decorations that will be drawn the ListView /// /// /// /// Do not modify the contents of this list directly. Use the AddDecoration() and RemoveDecoration() methods. /// /// /// A decoration scrolls with the list contents. An overlay is fixed in place. /// /// [Browsable(false)] protected IList Decorations { get { return this.decorations; } } private readonly List decorations = new List(); /// /// 所有者绘制时,此渲染器将绘制未指定特定渲染器的列 /// /// 如果尝试将其设置为NULL,则它将恢复为HighlightTextRenende [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IRenderer DefaultRenderer { get { return this.defaultRenderer; } set { this.defaultRenderer = value ?? new HighlightTextRenderer(); } } private IRenderer defaultRenderer = new HighlightTextRenderer(); /// /// 获取用于绘制给定单元格的渲染器。 /// /// The row model for the row /// The column to be drawn /// The renderer used for drawing a cell. Must not return null. public IRenderer GetCellRenderer(object model, OLVColumn column) { IRenderer renderer = this.CellRendererGetter == null ? null : this.CellRendererGetter(model, column); return renderer ?? column.Renderer ?? this.DefaultRenderer; } /// /// 获取或设置将应用于禁用项的样式。 /// /// If this is not set explicitly, will be used. [Category("ObjectListView"), Description("获取或设置将应用于禁用项的样式。"), DefaultValue(null)] public SimpleItemStyle DisabledItemStyle { get { return disabledItemStyle; } set { disabledItemStyle = value; } } private SimpleItemStyle disabledItemStyle; /// ///获取或设置已禁用的模型对象的列表。无法选择或激活已禁用的对象。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IEnumerable DisabledObjects { get { return disabledObjects.Keys; } set { this.disabledObjects.Clear(); DisableObjects(value); } } private readonly Hashtable disabledObjects = new Hashtable(); /// /// 判断此给定模型对象是否已禁用 /// /// /// public bool IsDisabled(object model) { return model != null && this.disabledObjects.ContainsKey(model); } /// /// 禁用给定的模型对象。禁用的对象无法选择或激活。 /// /// Must not be null public void DisableObject(object model) { ArrayList list = new ArrayList(); list.Add(model); this.DisableObjects(list); } /// ///禁用所有给定的模型对象 /// /// public void DisableObjects(IEnumerable models) { if (models == null) return; ArrayList list = ObjectListView.EnumerableToArray(models, false); foreach (object model in list) { if (model == null) continue; this.disabledObjects[model] = true; int modelIndex = this.IndexOf(model); if (modelIndex >= 0) NativeMethods.DeselectOneItem(this, modelIndex); } this.RefreshObjects(list); } /// ///启用给定的模型对象,以便可以再次选择并激活它。 /// /// Must not be null public void EnableObject(object model) { this.disabledObjects.Remove(model); this.RefreshObject(model); } /// ///启用所有给定的模型对象 /// /// public void EnableObjects(IEnumerable models) { if (models == null) return; ArrayList list = ObjectListView.EnumerableToArray(models, false); foreach (object model in list) { if (model != null) this.disabledObjects.Remove(model); } this.RefreshObjects(list); } /// ///忽略所有禁用的对象。这不会触发重绘或重建 /// protected void ClearDisabledObjects() { this.disabledObjects.Clear(); } /// /// Gets or sets the object that controls how drags start from this control /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IDragSource DragSource { get { return this.dragSource; } set { this.dragSource = value; } } private IDragSource dragSource; /// /// Gets or sets the object that controls how drops are accepted and processed /// by this ListView. /// /// /// /// If the given sink is an instance of SimpleDropSink, then events from the drop sink /// will be automatically forwarded to the ObjectListView (which means that handlers /// for those event can be configured within the IDE). /// /// If this is set to null, the control will not accept drops. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IDropSink DropSink { get { return this.dropSink; } set { if (this.dropSink == value) return; // Stop listening for events on the old sink SimpleDropSink oldSink = this.dropSink as SimpleDropSink; if (oldSink != null) { oldSink.CanDrop -= new EventHandler(this.DropSinkCanDrop); oldSink.Dropped -= new EventHandler(this.DropSinkDropped); oldSink.ModelCanDrop -= new EventHandler(this.DropSinkModelCanDrop); oldSink.ModelDropped -= new EventHandler(this.DropSinkModelDropped); } this.dropSink = value; this.AllowDrop = (value != null); if (this.dropSink != null) this.dropSink.ListView = this; // Start listening for events on the new sink SimpleDropSink newSink = value as SimpleDropSink; if (newSink != null) { newSink.CanDrop += new EventHandler(this.DropSinkCanDrop); newSink.Dropped += new EventHandler(this.DropSinkDropped); newSink.ModelCanDrop += new EventHandler(this.DropSinkModelCanDrop); newSink.ModelDropped += new EventHandler(this.DropSinkModelDropped); } } } private IDropSink dropSink; // Forward events from the drop sink to the control itself void DropSinkCanDrop(object sender, OlvDropEventArgs e) { this.OnCanDrop(e); } void DropSinkDropped(object sender, OlvDropEventArgs e) { this.OnDropped(e); } void DropSinkModelCanDrop(object sender, ModelDropEventArgs e) { this.OnModelCanDrop(e); } void DropSinkModelDropped(object sender, ModelDropEventArgs e) { this.OnModelDropped(e); } /// /// This registry decides what control should be used to edit what cells, based /// on the type of the value in the cell. /// /// /// All instances of ObjectListView share the same editor registry. // ReSharper disable FieldCanBeMadeReadOnly.Global static public EditorRegistry EditorRegistry = new EditorRegistry(); // ReSharper restore FieldCanBeMadeReadOnly.Global /// /// 获取或设置在此列表视图中没有项时应显示的文本。 /// /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, /// this property does nothing [Category("ObjectListView"), Description("当列表没有项时,在控件中显示此消息"), DefaultValue(null), Localizable(true)] public virtual String EmptyListMsg { get { TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; return overlay == null ? null : overlay.Text; } set { TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; if (overlay != null) { overlay.Text = value; this.Invalidate(); } } } /// /// 获取或设置绘制列表空消息时应使用的字体 /// /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, /// this property does nothing [Category("ObjectListView"), Description("获取或设置绘制列表空消息时应使用的字体"), DefaultValue(null)] public virtual Font EmptyListMsgFont { get { TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; return overlay == null ? null : overlay.Font; } set { TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; if (overlay != null) overlay.Font = value; } } /// /// 返回“列表为空”消息的字体或合理的默认值 /// [Browsable(false)] public virtual Font EmptyListMsgFontOrDefault { get { return this.EmptyListMsgFont ?? new Font("Tahoma", 14); } } /// /// Gets or sets the overlay responsible for drawing the List Empty msg. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IOverlay EmptyListMsgOverlay { get { return this.emptyListMsgOverlay; } set { if (this.emptyListMsgOverlay != value) { this.emptyListMsgOverlay = value; this.Invalidate(); } } } private IOverlay emptyListMsgOverlay; /// ///获取在筛选中幸存下来的对象的集合。 /// /// /// /// This collection is the result of filtering the current list of objects. /// It is not a snapshot of the filtered list that was last used to build the control. /// /// /// Normal warnings apply when using this with virtual lists. It will work, but it /// may take a while. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] virtual public IEnumerable FilteredObjects { get { if (this.UseFiltering) return this.FilterObjects(this.Objects, this.ModelFilter, this.ListFilter); return this.Objects; } } /// /// 获取或设置将用于生成筛选器菜单的策略对象 /// /// 如果为NULL,则不会生成筛选器菜单。 [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public FilterMenuBuilder FilterMenuBuildStrategy { get { return filterMenuBuilder; } set { filterMenuBuilder = value; } } private FilterMenuBuilder filterMenuBuilder = new FilterMenuBuilder(); /// /// 获取或设置具有键盘焦点的行 /// /// /// /// Setting an object to be focused does *not* select it. If you want to select and focus a row, /// use . /// /// /// This property is not generally used and is only useful in specialized situations. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual Object FocusedObject { get { return this.FocusedItem == null ? null : ((OLVListItem)this.FocusedItem).RowObject; } set { OLVListItem item = this.ModelToItem(value); if (item != null) item.Focused = true; } } /// /// 隐藏Groups集合,使其在“属性”网格中不可见。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] new public ListViewGroupCollection Groups { get { return base.Groups; } } /// ///获取或设置分组使用的ImageList /// /// 如果未设置此选项,则分组标题将不会显示任何图像。 [Category("ObjectListView"), Description("获取或设置分组使用的ImageList"), DefaultValue(null)] public ImageList GroupImageList { get { return this.groupImageList; } set { this.groupImageList = value; if (this.Created) { NativeMethods.SetGroupImageList(this, value); } } } private ImageList groupImageList; /// /// 获取在组为空或包含多个项目时应如何设置组标签的格式 /// /// /// 给定的格式字符串必须有两个占位符: /// /// {0} - 原来的组标题 /// {1} - 组中的项目数 /// /// /// "[{0} [{1} 项]" [Category("ObjectListView"), Description("获取在组为空或包含多个项目时应如何设置组标签的格式"), DefaultValue(null), Localizable(true)] public virtual string GroupWithItemCountFormat { get { return groupWithItemCountFormat; } set { groupWithItemCountFormat = value; } } private string groupWithItemCountFormat; /// /// 返回this.GroupWithItemCountFormat或合理的默认值 /// [Browsable(false)] public virtual string GroupWithItemCountFormatOrDefault { get { return String.IsNullOrEmpty(this.GroupWithItemCountFormat) ? "{0} [{1} 项]" : this.GroupWithItemCountFormat; } } /// /// 获取在组仅包含单个项目时应如何设置组标签的格式 /// /// ///给定的格式字符串必须有两个占位符: /// /// {0} - 原来的组标题 /// {1} - 组中的项目数 (总是1) /// /// /// "[{0} [{1} 项]" [Category("ObjectListView"), Description("获取在组仅包含单个项目时应如何设置组标签的格式"), DefaultValue(null), Localizable(true)] public virtual string GroupWithItemCountSingularFormat { get { return groupWithItemCountSingularFormat; } set { groupWithItemCountSingularFormat = value; } } private string groupWithItemCountSingularFormat; /// /// 获取GroupWithItemCountSingularFormat或合理的默认值 /// [Browsable(false)] public virtual string GroupWithItemCountSingularFormatOrDefault { get { return String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat) ? "{0} [{1} 项]" : this.GroupWithItemCountSingularFormat; } } /// ///获取或设置此ObjectListView中的组是否应可折叠。 /// /// /// 此特性只支持Vista或之后的系统. /// [Browsable(true), Category("ObjectListView"), Description("获取或设置此ObjectListView中的组是否应可折叠 (Vista或之后的系统才支持)."), DefaultValue(true)] public bool HasCollapsibleGroups { get { return hasCollapsibleGroups; } set { hasCollapsibleGroups = value; } } private bool hasCollapsibleGroups = true; /// /// 获取当前列表是否存在当列表为空时显示的文本 /// [Browsable(false)] public virtual bool HasEmptyListMsg { get { return !String.IsNullOrEmpty(this.EmptyListMsg); } } /// /// 获取是否有要绘制的叠加层 /// [Browsable(false)] public bool HasOverlays { get { return (this.Overlays.Count > 2 || this.imageOverlay.Image != null || !String.IsNullOrEmpty(this.textOverlay.Text)); } } /// /// 获取ListView的列头控件 /// [Browsable(false)] public HeaderControl HeaderControl { get { return this.headerControl ?? (this.headerControl = new HeaderControl(this)); } } private HeaderControl headerControl; /// /// 获取或设置列标题文本的绘制字体 /// /// 单个列可以通过其HeaderFormatStyle属性重写此设置。 [DefaultValue(null)] [Browsable(false)] [Obsolete("请改用HeaderFormatStyle", false)] public Font HeaderFont { get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } set { if (value == null && this.HeaderFormatStyle == null) return; if (this.HeaderFormatStyle == null) this.HeaderFormatStyle = new HeaderFormatStyle(); this.HeaderFormatStyle.SetFont(value); } } /// /// 获取或设置将用于绘制列表视图的列标题的样式 /// /// /// /// 仅当HeaderUsesThemes为false时才使用此选项。 /// /// /// 单个列可以通过其HeaderFormatStyle属性重写此设置。 /// /// [Category("ObjectListView"), Description("获取或设置将用于绘制列表视图的列标题的样式"), DefaultValue(null)] public HeaderFormatStyle HeaderFormatStyle { get { return this.headerFormatStyle; } set { this.headerFormatStyle = value; } } private HeaderFormatStyle headerFormatStyle; /// /// 获取或设置标题的最大高度。-1表示没有最大值。 /// [Category("ObjectListView"), Description("获取或设置标题的最大高度。-1表示没有最大值。"), DefaultValue(-1)] public int HeaderMaximumHeight { get { return headerMaximumHeight; } set { headerMaximumHeight = value; } } private int headerMaximumHeight = -1; /// /// 获取或设置标题的最小高度。-1表示没有最小值。 /// [Category("ObjectListView"), Description("获取或设置标题的最小高度。-1表示没有最小值。"), DefaultValue(-1)] public int HeaderMinimumHeight { get { return headerMinimumHeight; } set { headerMinimumHeight = value; } } private int headerMinimumHeight = -1; /// /// 获取或设置是否严格按照操作系统的主题绘制标题。 /// /// /// ///如果设置为true,则列头将完全由系统呈现,不需要ObjectListViews的任何特殊处理。 ///页眉中将没有图像、没有过滤器指示符、没有文字换行、没有页眉样式、没有复选框。 /// /// 如果将其设置为False,ObjectListView将以其认为最好的方式呈现标题。 /// 如果不需要特殊功能,则ObjectListView会将渲染委托给操作系统。 /// 否则,ObjectListView将根据配置设置绘制标题。 /// /// /// 没有主题的效果会因操作系统不同而不同。至少,分类指示器不会是标准的。 /// /// [Category("ObjectListView"), Description("获取或设置是否严格按照操作系统的主题绘制标题"), DefaultValue(false)] public bool HeaderUsesThemes { get { return this.headerUsesThemes; } set { this.headerUsesThemes = value; } } private bool headerUsesThemes; /// /// 获取或设置列头标题中的文本是否自动换行。 /// /// /// 将在单词之间应用换行符。过长的单词仍将被省略。 /// /// 与所有使列头看起来不同的设置一样,HeaderUsesThemes必须设置为false,否则操作系统将负责绘制列头,并且不允许自动换行文本。 /// /// [Category("ObjectListView"), Description("Will the text of the column headers be word wrapped?"), DefaultValue(false)] public bool HeaderWordWrap { get { return this.headerWordWrap; } set { this.headerWordWrap = value; if (this.headerControl != null) this.headerControl.WordWrap = value; } } private bool headerWordWrap; /// /// 获取显示列标题提示的工具提示 /// [Browsable(false)] public ToolTipControl HeaderToolTip { get { return this.HeaderControl.ToolTip; } } /// ///获取鼠标当前所在行的索引。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual int HotRowIndex { get { return this.hotRowIndex; } protected set { this.hotRowIndex = value; } } private int hotRowIndex; /// /// 获取鼠标当前所在列的索引 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual int HotColumnIndex { get { return this.hotColumnIndex; } protected set { this.hotColumnIndex = value; } } private int hotColumnIndex; /// /// 获取鼠标当前所在的项/子项的一部分 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual HitTestLocation HotCellHitLocation { get { return this.hotCellHitLocation; } protected set { this.hotCellHitLocation = value; } } private HitTestLocation hotCellHitLocation; /// /// Gets an extended indication of the part of item/subitem/group that the mouse is currently over /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual HitTestLocationEx HotCellHitLocationEx { get { return this.hotCellHitLocationEx; } protected set { this.hotCellHitLocationEx = value; } } private HitTestLocationEx hotCellHitLocationEx; /// /// 获取鼠标所在的组。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVGroup HotGroup { get { return hotGroup; } internal set { hotGroup = value; } } private OLVGroup hotGroup; /// ///处于“热”状态的项的索引,即在光标下方。-1表示没有项目。 /// [Browsable(false), Obsolete("请使用 HotRowIndex 代替", false)] public virtual int HotItemIndex { get { return this.HotRowIndex; } } /// /// 获取和设置对游标下的行应用哪种格式 /// /// /// ///这仅在UseHotItem为true时生效。 /// /// 如果样式具有叠加层,则必须对其进行设置。*之前* 将其分配给此属性。之后添加它将被忽略。 /// [Category("ObjectListView"), Description("获取和设置对游标下的行应用哪种格式"), DefaultValue(null)] public virtual HotItemStyle HotItemStyle { get { return this.hotItemStyle; } set { if (this.HotItemStyle != null) this.RemoveOverlay(this.HotItemStyle.Overlay); this.hotItemStyle = value; if (this.HotItemStyle != null) this.AddOverlay(this.HotItemStyle.Overlay); } } private HotItemStyle hotItemStyle; /// /// 获取已安装的热点项样式或合理的默认值。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual HotItemStyle HotItemStyleOrDefault { get { return this.HotItemStyle ?? ObjectListView.DefaultHotItemStyle; } } /// /// 获取和设置应将哪种格式应用于超链接 /// [Category("ObjectListView"), Description("获取和设置应将哪种格式应用于超链接"), DefaultValue(null)] public virtual HyperlinkStyle HyperlinkStyle { get { return this.hyperlinkStyle; } set { this.hyperlinkStyle = value; } } private HyperlinkStyle hyperlinkStyle; /// /// 获取和设置所选行的背景应该使用什么颜色 /// [Category("ObjectListView"), Description("获取和设置所选行的背景应该使用什么颜色"), DefaultValue(typeof(Color), "")] public virtual Color SelectedBackColor { get { return this.selectedBackColor; } set { this.selectedBackColor = value; } } private Color selectedBackColor = Color.Empty; /// /// 返回应用于选定行的背景颜色或合理的默认值 /// [Browsable(false)] public virtual Color SelectedBackColorOrDefault { get { return this.SelectedBackColor.IsEmpty ? SystemColors.Highlight : this.SelectedBackColor; } } /// /// 获取和设置所选行的字体应该使用什么颜色 /// [Category("ObjectListView"), Description("获取和设置所选行的字体应该使用什么颜色"), DefaultValue(typeof(Color), "")] public virtual Color SelectedForeColor { get { return this.selectedForeColor; } set { this.selectedForeColor = value; } } private Color selectedForeColor = Color.Empty; /// /// 返回应用于选定行的字体颜色或合理的默认值 /// [Browsable(false)] public virtual Color SelectedForeColorOrDefault { get { return this.SelectedForeColor.IsEmpty ? SystemColors.HighlightText : this.SelectedForeColor; } } /// /// 使用 SelectedBackColor 代替 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [Obsolete("使用 SelectedBackColor 代替")] public virtual Color HighlightBackgroundColor { get { return this.SelectedBackColor; } set { this.SelectedBackColor = value; } } /// /// 使用 SelectedBackColorOrDefault 代替 /// [Obsolete("使用 SelectedBackColorOrDefault 代替")] public virtual Color HighlightBackgroundColorOrDefault { get { return this.SelectedBackColorOrDefault; } } /// /// 使用 SelectedForeColor 代替 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [Obsolete("使用 SelectedForeColor 代替")] public virtual Color HighlightForegroundColor { get { return this.SelectedForeColor; } set { this.SelectedForeColor = value; } } /// /// 使用 SelectedForeColorOrDefault 代替 /// [Obsolete("使用 SelectedForeColorOrDefault 代替")] public virtual Color HighlightForegroundColorOrDefault { get { return this.SelectedForeColorOrDefault; } } /// /// 使用 UnfocusedSelectedBackColor 代替 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [Obsolete("使用 UnfocusedSelectedBackColor 代替")] public virtual Color UnfocusedHighlightBackgroundColor { get { return this.UnfocusedSelectedBackColor; } set { this.UnfocusedSelectedBackColor = value; } } /// /// 使用 UnfocusedSelectedBackColorOrDefault 代替 /// [Obsolete("使用 UnfocusedSelectedBackColorOrDefault 代替")] public virtual Color UnfocusedHighlightBackgroundColorOrDefault { get { return this.UnfocusedSelectedBackColorOrDefault; } } /// /// 使用 UnfocusedSelectedForeColor 代替 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [Obsolete("使用 UnfocusedSelectedForeColor 代替")] public virtual Color UnfocusedHighlightForegroundColor { get { return this.UnfocusedSelectedForeColor; } set { this.UnfocusedSelectedForeColor = value; } } /// /// 使用 UnfocusedSelectedForeColorOrDefault 代替 /// [Obsolete("使用 UnfocusedSelectedForeColorOrDefault 代替")] public virtual Color UnfocusedHighlightForegroundColorOrDefault { get { return this.UnfocusedSelectedForeColorOrDefault; } } /// /// 获取或设置是否应将隐藏列包括在复制或拖动到其他应用程序的行的文本表示形式中。 /// 如果为False(默认值),则仅包括可见列。 /// [Category("ObjectListView"), Description("获取或设置是否应将隐藏列包括在复制或拖动到其他应用程序的行的文本表示形式中。如果为False(默认值),则仅包括可见列。"), DefaultValue(false)] public virtual bool IncludeHiddenColumnsInDataTransfer { get { return includeHiddenColumnsInDataTransfer; } set { includeHiddenColumnsInDataTransfer = value; } } private bool includeHiddenColumnsInDataTransfer; /// /// 获取或设置复制行时,列标题是否在文本中。 /// 如果为False(默认值),在文本中将不会显示列标题。 /// [Category("ObjectListView"), Description("复制行时,列标题是否在文本中."), DefaultValue(false)] public virtual bool IncludeColumnHeadersInCopy { get { return includeColumnHeadersInCopy; } set { includeColumnHeadersInCopy = value; } } private bool includeColumnHeadersInCopy; /// ///如果当前正在进行单元格编辑操作,则返回TRUE /// [Browsable(false)] public virtual bool IsCellEditing { get { return this.cellEditor != null; } } /// /// 如果在开发环境中使用ObjectListView,则返回true。 /// [Browsable(false)] public virtual bool IsDesignMode { get { return this.DesignMode; } } /// ///获取当前列表是否正在筛选其内容 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] virtual public bool IsFiltering { get { return this.UseFiltering && (this.ModelFilter != null || this.ListFilter != null); } } /// ///当用户在列表中键入内容时,是否应该搜索当前排序列中的值以查找匹配项? ///如果为False,则无论排序列是什么,都将始终使用主列。 /// /// When this is true, the behavior is like that of ITunes. [Category("ObjectListView"), Description("当用户在列表中键入内容时,是否应该搜索当前排序列中的值以查找匹配项?如果为False,则无论排序列是什么,都将始终使用主列。"), DefaultValue(true)] public virtual bool IsSearchOnSortColumn { get { return isSearchOnSortColumn; } set { isSearchOnSortColumn = value; } } private bool isSearchOnSortColumn = true; /// /// Gets or sets if this control will use a SimpleDropSink to receive drops /// /// /// /// Setting this replaces any previous DropSink. /// /// /// After setting this to true, the SimpleDropSink will still need to be configured /// to say when it can accept drops and what should happen when something is dropped. /// The need to do these things makes this property mostly useless :( /// /// [Category("ObjectListView"), Description("Should this control will use a SimpleDropSink to receive drops."), DefaultValue(false)] public virtual bool IsSimpleDropSink { get { return this.DropSink != null; } set { this.DropSink = value ? new SimpleDropSink() : null; } } /// /// Gets or sets if this control will use a SimpleDragSource to initiate drags /// /// Setting this replaces any previous DragSource [Category("ObjectListView"), Description("Should this control use a SimpleDragSource to initiate drags out from this control"), DefaultValue(false)] public virtual bool IsSimpleDragSource { get { return this.DragSource != null; } set { this.DragSource = value ? new SimpleDragSource() : null; } } /// /// 隐藏Items集合,使其在“属性”网格中不可见。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] new public ListViewItemCollection Items { get { return base.Items; } } /// /// This renderer draws the items when in the list is in non-details view. /// In details view, the renderers for the individuals columns are responsible. /// [Category("ObjectListView"), Description("The owner drawn renderer that draws items when the list is in non-Details view."), DefaultValue(null)] public IRenderer ItemRenderer { get { return itemRenderer; } set { itemRenderer = value; } } private IRenderer itemRenderer; /// /// 获取或设置最后一次排序的列 /// /// 这是PrimarySortColumn的别名 [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OLVColumn LastSortColumn { get { return this.PrimarySortColumn; } set { this.PrimarySortColumn = value; } } /// /// 获取或设置最后一次排序的方向 /// /// 这是 PrimarySortOrder的别名 [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual SortOrder LastSortOrder { get { return this.PrimarySortOrder; } set { this.PrimarySortOrder = value; } } /// /// 获取或设置应用于整个对象列表的筛选器。 /// /// /// 列表将立即更新以反映此筛选器。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IListFilter ListFilter { get { return listFilter; } set { listFilter = value; if (this.UseFiltering) this.UpdateFiltering(); } } private IListFilter listFilter; /// /// 获取或设置应用于列表中每个模型对象的筛选器 /// /// /// 您可能需要考虑使用 而不是此属性, /// 因为AdditionalFilter在运行时与列筛选相结合。 /// 设置此属性只是替换用户可能已给出的任何列筛选器。 /// /// The list is updated immediately to reflect this filter. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IModelFilter ModelFilter { get { return modelFilter; } set { modelFilter = value; this.NotifyNewModelFilter(); if (this.UseFiltering) { this.UpdateFiltering(); // When the filter changes, it's likely/possible that the selection has also changed. // It's expensive to see if the selection has actually changed (for large virtual lists), // so we just fake a selection changed event, just in case. SF #144 this.OnSelectedIndexChanged(EventArgs.Empty); } } } private IModelFilter modelFilter; /// /// Gets the hit test info last time the mouse was moved. /// /// Useful for hot item processing. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OlvListViewHitTestInfo MouseMoveHitTest { get { return mouseMoveHitTest; } private set { mouseMoveHitTest = value; } } private OlvListViewHitTestInfo mouseMoveHitTest; /// ///获取或设置列表显示的组列表。 /// /// /// 此属性与.NET Groups属性的工作方式不同,应将其视为只读属性。 /// 对列表所做的更改不会反映在ListView本身中。 /// 在此列表中添加或删除组是没有意义的,这样的修改不会起到任何作用。 /// 要执行此类操作,必须侦听BeforeCreatingGroups或AboutToCreateGroups事件, /// 并更改这些事件中的组列表。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IList OLVGroups { get { return this.olvGroups; } set { this.olvGroups = value; } } private IList olvGroups; /// /// 获取或设置折叠的OLVGroups的集合。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IEnumerable CollapsedGroups { get { if (this.OLVGroups != null) { foreach (OLVGroup group in this.OLVGroups) { if (group.Collapsed) yield return group; } } } set { if (this.OLVGroups == null) return; Hashtable shouldCollapse = new Hashtable(); if (value != null) { foreach (OLVGroup group in value) shouldCollapse[group.Key] = true; } foreach (OLVGroup group in this.OLVGroups) { group.Collapsed = shouldCollapse.ContainsKey(group.Key); } } } /// /// Gets or sets whether the user wants to owner draw the header control /// themselves. If this is false (the default), ObjectListView will use /// custom drawing to render the header, if needed. /// /// /// If you listen for the DrawColumnHeader event, you need to set this to true, /// otherwise your event handler will not be called. /// [Category("ObjectListView"), Description("Should the DrawColumnHeader event be triggered"), DefaultValue(false)] public bool OwnerDrawnHeader { get { return ownerDrawnHeader; } set { ownerDrawnHeader = value; } } private bool ownerDrawnHeader; /// ///获取/设置此列表将显示的对象集合 /// /// /// /// The contents of the control will be updated immediately after setting this property. /// /// This method preserves selection, if possible. Use if /// you do not want to preserve the selection. Preserving selection is the slowest part of this /// code and performance is O(n) where n is the number of selected rows. /// This method is not thread safe. /// The property DOES work on virtual lists: setting is problem-free, but if you try to get it /// and the list has 10 million objects, it may take some time to return. /// This collection is unfiltered. Use to access just those objects /// that survive any installed filters. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IEnumerable Objects { get { return this.objects; } set { this.SetObjects(value, true); } } private IEnumerable objects; /// /// Gets the collection of objects that will be considered when creating clusters /// (which are used to generate Excel-like column filters) /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IEnumerable ObjectsForClustering { get { return this.Objects; } } /// /// 获取或设置将在ListView顶部绘制的图像 /// [Category("ObjectListView"), Description("获取或设置将在ListView顶部绘制的图像"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public ImageOverlay OverlayImage { get { return this.imageOverlay; } set { if (this.imageOverlay == value) return; this.RemoveOverlay(this.imageOverlay); this.imageOverlay = value; this.AddOverlay(this.imageOverlay); } } private ImageOverlay imageOverlay; /// /// 获取或设置将在ListView顶部绘制的文本 /// [Category("ObjectListView"), Description("获取或设置将在ListView顶部绘制的文本"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public TextOverlay OverlayText { get { return this.textOverlay; } set { if (this.textOverlay == value) return; this.RemoveOverlay(this.textOverlay); this.textOverlay = value; this.AddOverlay(this.textOverlay); } } private TextOverlay textOverlay; /// /// 获取或设置所有覆盖图的透明度。0表示完全透明,255表示完全不透明。 /// /// ///这已经过时了,请在每个覆盖上使用Transparency。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int OverlayTransparency { get { return this.overlayTransparency; } set { this.overlayTransparency = Math.Min(255, Math.Max(0, value)); } } private int overlayTransparency = 128; /// /// 获取将在ListView顶部绘制的覆盖列表 /// /// /// You can add new overlays and remove overlays that you have added, but /// don't mess with the overlays that you didn't create. /// [Browsable(false)] protected IList Overlays { get { return this.overlays; } } private readonly List overlays = new List(); /// /// Gets or sets whether the ObjectListView will be owner drawn. Defaults to true. /// /// /// /// When this is true, all of ObjectListView's neat features are available. /// /// We have to reimplement this property, even though we just call the base /// property, in order to change the [DefaultValue] to true. /// /// [Category("Appearance"), Description("Should the ListView do its own rendering"), DefaultValue(true)] public new bool OwnerDraw { get { return base.OwnerDraw; } set { base.OwnerDraw = value; } } /// /// 获取或设置主复选框是否在列表重新生成和筛选操作中保持其值。 /// /// /// /// 此属性仅在未显式设置CheckStateGetter/Putter时才有用。 /// 如果您使用CheckStateGetter/Putter,那么这些方法已经持久化了行的检查性。 /// /// 此默认值为true。如果为False,则在重新生成或筛选列表时,复选框将丢失其值。 /// 如果在虚拟列表中将其设置为false,则必须安装CheckStateGetter/Putters。 /// [Category("ObjectListView"), Description("获取或设置主复选框是否在列表重新生成和筛选操作中保持其值"), DefaultValue(true)] public virtual bool PersistentCheckBoxes { get { return persistentCheckBoxes; } set { if (persistentCheckBoxes == value) return; persistentCheckBoxes = value; this.ClearPersistentCheckState(); } } private bool persistentCheckBoxes = true; /// /// 获取或设置记住模型对象的复选框状态的字典 /// /// 当PersistentCheckBoxs值为TRUE时,此选项用于虚拟列表. protected Dictionary CheckStateMap { get { return checkStateMap ?? (checkStateMap = new Dictionary()); } set { checkStateMap = value; } } private Dictionary checkStateMap; /// /// 获取或设置最后一次排序的列 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OLVColumn PrimarySortColumn { get { return this.primarySortColumn; } set { this.primarySortColumn = value; if (this.TintSortColumn) this.SelectedColumn = value; } } private OLVColumn primarySortColumn; /// /// 获取或设置最后一次排序的方向 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual SortOrder PrimarySortOrder { get { return primarySortOrder; } set { primarySortOrder = value; } } private SortOrder primarySortOrder; /// /// 获取或设置不可编辑复选框是否绘制为禁用。默认是false。 /// /// /// 这仅在所有者描述模式(owner drawn mode)下有效。 /// [Category("ObjectListView"), Description("获取或设置不可编辑复选框是否绘制为禁用"), DefaultValue(false)] public virtual bool RenderNonEditableCheckboxesAsDisabled { get { return renderNonEditableCheckboxesAsDisabled; } set { renderNonEditableCheckboxesAsDisabled = value; } } private bool renderNonEditableCheckboxesAsDisabled; /// /// 以像素为单位指定控件中每行的高度。 /// /// 列表视图中的行高通常由字体大小和小图像列表大小决定。 /// 此设置允许覆盖该计算(原因是:您仍然不能将行高设置为小于控件中使用的字体的行高)。 /// 将其设置为-1表示使用正常计算方法。 /// 此功能是体验功能!如果您使用此功能,您的程序可能会发生意想不到的事情。 /// [Category("ObjectListView"), Description("以像素为单位指定控件中每行的高度"), DefaultValue(-1)] public virtual int RowHeight { get { return rowHeight; } set { if (value < 1) rowHeight = -1; else rowHeight = value; if (this.DesignMode) return; this.SetupBaseImageList(); if (this.CheckBoxes) this.InitializeStateImageList(); } } private int rowHeight = -1; /// /// 获取每行有多少像素高 /// [Browsable(false)] public virtual int RowHeightEffective { get { switch (this.View) { case View.List: case View.SmallIcon: case View.Details: return Math.Max(this.SmallImageSize.Height, this.Font.Height); case View.Tile: return this.TileSize.Height; case View.LargeIcon: if (this.LargeImageList == null) return this.Font.Height; return Math.Max(this.LargeImageList.ImageSize.Height, this.Font.Height); default: // This should never happen return 0; } } } /// /// 获取此控件的每页上显示多少行 /// [Browsable(false)] public virtual int RowsPerPage { get { return NativeMethods.GetCountPerPage(this); } } /// ///获取/设置将用于解析排序时相等的比较的列(第二排序列)。 /// /// 此设置没有用户界面。它必须以编程方式设置。 [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OLVColumn SecondarySortColumn { get { return this.secondarySortColumn; } set { this.secondarySortColumn = value; } } private OLVColumn secondarySortColumn; /// /// 获取或设置当使用Second darySortColumn时,它将以什么顺序比较结果 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual SortOrder SecondarySortOrder { get { return this.secondarySortOrder; } set { this.secondarySortOrder = value; } } private SortOrder secondarySortOrder = SortOrder.None; /// ///获取或设置当用户按Ctrl+A组合键时是否应选择所有行 /// [Category("ObjectListView"), Description("获取或设置当用户按Ctrl+A组合键时是否应选择所有行"), DefaultValue(true)] public virtual bool SelectAllOnControlA { get { return selectAllOnControlA; } set { selectAllOnControlA = value; } } private bool selectAllOnControlA = true; /// /// 获取或设置当用户右键单击列标题时,是否应该显示一个菜单,允许用户选择视图中将显示哪些列 /// /// 这只是SelectColumnsOnRightClickBehaviour的兼容性包装属性. [Category("ObjectListView"), Description("获取或设置当用户右键单击列标题时,是否应该显示一个菜单,允许用户选择视图中将显示哪些列"), DefaultValue(true)] public virtual bool SelectColumnsOnRightClick { get { return this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None; } set { if (value) { if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.None) this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; } else { this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.None; } } } /// /// 获取或设置当右键单击标题时用户如何能够选择列 /// [Category("ObjectListView"), Description("获取或设置当右键单击标题时用户如何能够选择列"), DefaultValue(ColumnSelectBehaviour.InlineMenu)] public virtual ColumnSelectBehaviour SelectColumnsOnRightClickBehaviour { get { return selectColumnsOnRightClickBehaviour; } set { selectColumnsOnRightClickBehaviour = value; } } private ColumnSelectBehaviour selectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; /// /// 当列选择菜单打开时,是否应在选择项目后保持打开状态?保持打开状态允许用户一次打开或关闭多个列。 /// /// SelectColumnsOnRightClickBehaviour设置为InlineMenu时才有效。当行为设置为SubMenu菜单时,该选项不起作用。 [Category("ObjectListView"), Description("当列选择菜单打开时,是否应在选择项目后保持打开状态?保持打开状态允许用户一次打开或关闭多个列。"), DefaultValue(true)] public virtual bool SelectColumnsMenuStaysOpen { get { return selectColumnsMenuStaysOpen; } set { selectColumnsMenuStaysOpen = value; } } private bool selectColumnsMenuStaysOpen = true; /// /// Gets or sets the column that is drawn with a slight tint. /// /// /// /// If TintSortColumn is true, the sort column will automatically /// be made the selected column. /// /// /// The colour of the tint is controlled by SelectedColumnTint. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OLVColumn SelectedColumn { get { return this.selectedColumn; } set { this.selectedColumn = value; if (value == null) { this.RemoveDecoration(this.selectedColumnDecoration); } else { if (!this.HasDecoration(this.selectedColumnDecoration)) this.AddDecoration(this.selectedColumnDecoration); } } } private OLVColumn selectedColumn; private readonly TintedColumnDecoration selectedColumnDecoration = new TintedColumnDecoration(); /// /// Gets or sets the decoration that will be drawn on all selected rows /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IDecoration SelectedRowDecoration { get { return this.selectedRowDecoration; } set { this.selectedRowDecoration = value; } } private IDecoration selectedRowDecoration; /// /// 获取或设置应该使用什么颜色来给选定的列着色 /// /// /// 色调颜色必须是Alpha可混合的,因此如果给定的颜色是纯色(即Alpha=255),则会将其更改为具有合理的Alpha值。 /// [Category("ObjectListView"), Description("获取或设置应该使用什么颜色来给选定的列着色"), DefaultValue(typeof(Color), "")] public virtual Color SelectedColumnTint { get { return selectedColumnTint; } set { this.selectedColumnTint = value.A == 255 ? Color.FromArgb(15, value) : value; this.selectedColumnDecoration.Tint = this.selectedColumnTint; } } private Color selectedColumnTint = Color.Empty; /// /// 获取或设置当前选定行的索引。 /// 获取索引时,如果没有选择任何行,或者选择了多个行,则返回-1。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual int SelectedIndex { get { return this.SelectedIndices.Count == 1 ? this.SelectedIndices[0] : -1; } set { this.SelectedIndices.Clear(); if (value >= 0 && value < this.Items.Count) this.SelectedIndices.Add(value); } } /// ///获取或设置当前选定的ListViewItem。如果未选择任何行,或者选择了多个行,则返回NULL。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual OLVListItem SelectedItem { get { return this.SelectedIndices.Count == 1 ? this.GetItem(this.SelectedIndices[0]) : null; } set { this.SelectedIndices.Clear(); if (value != null) this.SelectedIndices.Add(value.Index); } } /// /// 如果只选择了一行,则从当前选定的行获取模型对象。 /// 如果未选择任何行,或选择了多个行,则返回NULL。 /// 设置时,这将选择显示给定模型对象的行,并将焦点放在该行上,取消选择所有其他行。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual Object SelectedObject { get { return this.SelectedIndices.Count == 1 ? this.GetModelObject(this.SelectedIndices[0]) : null; } set { // If the given model is already selected, don't do anything else (prevents an flicker) object selectedObject = this.SelectedObject; if (selectedObject != null && selectedObject.Equals(value)) return; this.SelectedIndices.Clear(); this.SelectObject(value, true); } } /// /// 从当前选定的行中获取模型对象。 /// 如果未选中任何行,则返回的列表为空。 /// 设置此值时,请选择显示给定模型对象的行,取消选择所有其他行。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IList SelectedObjects { get { ArrayList list = new ArrayList(); foreach (int index in this.SelectedIndices) list.Add(this.GetModelObject(index)); return list; } set { this.SelectedIndices.Clear(); this.SelectObjects(value); } } /// /// 获取或设置当用户右键单击列标题时,是否应该显示一个菜单,允许他们选择要在列表视图上执行的常见任务 /// [Category("ObjectListView"), Description("获取或设置当用户右键单击列标题时,是否应该显示一个菜单,允许他们选择要在列表视图上执行的常见任务"), DefaultValue(false)] public virtual bool ShowCommandMenuOnRightClick { get { return showCommandMenuOnRightClick; } set { showCommandMenuOnRightClick = value; } } private bool showCommandMenuOnRightClick; /// /// 获取或设置当右键单击标题控件时,此ObjectListView是否像筛选菜单一样显示表格 /// [Category("ObjectListView"), Description("如果为True,则右键单击列标题将显示筛选器菜单选项"), DefaultValue(true)] public bool ShowFilterMenuOnRightClick { get { return showFilterMenuOnRightClick; } set { showFilterMenuOnRightClick = value; } } private bool showFilterMenuOnRightClick = true; /// /// 获取或设置是否应按组显示其项目 /// [Category("Appearance"), Description("获取或设置是否应按组显示其项目"), DefaultValue(true)] new public virtual bool ShowGroups { get { return base.ShowGroups; } set { this.GroupImageList = this.GroupImageList; base.ShowGroups = value; } } /// /// 列表视图是否应该在列标题中显示位图以显示排序方向 /// /// /// The only reason for not wanting to have sort indicators is that, on pre-XP versions of /// Windows, having sort indicators required the ListView to have a small image list, and /// as soon as you give a ListView a SmallImageList, the text of column 0 is bumped 16 /// pixels to the right, even if you never used an image. /// [Category("ObjectListView"), Description("列表视图是否应该在列标题中显示位图以显示排序方向"), DefaultValue(true)] public virtual bool ShowSortIndicators { get { return showSortIndicators; } set { showSortIndicators = value; } } private bool showSortIndicators; /// /// 列表视图是否应该显示子项上的图像 /// /// /// 虚拟列表必须是所有者绘制的,才能在子项上显示图像 /// [Category("ObjectListView"), Description("列表视图是否应该显示子项上的图像"), DefaultValue(false)] public virtual bool ShowImagesOnSubItems { get { return showImagesOnSubItems; } set { showImagesOnSubItems = value; if (this.Created) this.ApplyExtendedStyles(); if (value && this.VirtualMode) this.OwnerDraw = true; } } private bool showImagesOnSubItems; /// /// 此属性控制分组标签是否显示该分组项目数量 /// /// ///后缀的格式由GroupWithItemCountFormat/GroupWithItemCountSingularFormat属性控制 /// [Category("ObjectListView"), Description("此属性控制分组标签是否显示该分组项目数量"), DefaultValue(false)] public virtual bool ShowItemCountOnGroups { get { return showItemCountOnGroups; } set { showItemCountOnGroups = value; } } private bool showItemCountOnGroups; /// /// 获取或设置控件是在所有视图中显示列标题(True),还是仅在详细信息视图中显示列标题(False) /// /// /// /// 此属性工作不正常。JPP 2010/04/06。 /// 如果它是在创建控件之前设置的,则可以正常工作。 /// 但是,如果在创建控件后将其关闭,则控件将丢失其复选框(奇怪!) /// /// /// 要在创建控件后更改此设置,事情会很复杂。 /// 如果它是关闭的,而我们想要打开它,我们必须更改视图,标题就会出现。 /// 如果它当前处于打开状态,而我们想要将其关闭,则必须更改视图并重新创建控制柄。 /// 重新创建句柄是一个问题,因为它会使我们的复选框样式消失。 /// /// /// 该属性不能工作在 Win XP 系统. /// [Category("ObjectListView"), Description("获取或设置控件是在所有视图中显示列标题(True),还是仅在详细信息视图中显示列标题(False)"), DefaultValue(true)] public bool ShowHeaderInAllViews { get { return ObjectListView.IsVistaOrLater && showHeaderInAllViews; } set { if (showHeaderInAllViews == value) return; showHeaderInAllViews = value; // If the control isn't already created, everything is fine. if (!this.Created) return; // If the header is being hidden, we have to recreate the control // to remove the style (not sure why this is) if (showHeaderInAllViews) this.ApplyExtendedStyles(); else this.RecreateHandle(); // Still more complications. The change doesn't become visible until the View is changed if (this.View != View.Details) { View temp = this.View; this.View = View.Details; this.View = temp; } } } private bool showHeaderInAllViews = true; /// ///重写SmallImageList属性,以便我们可以正确地隐藏其操作。 /// /// 如果使用RowHeight属性指定行高,则必须在设置/更改RowHeight之前完全初始化SmallImageList。 /// 如果在设置行高之后将新图像添加到图像列表,则必须再次将图像列表分配给控件。像这样简单的事情会奏效的: /// listView1.SmallImageList = listView1.SmallImageList; /// new public ImageList SmallImageList { get { return this.shadowedImageList; } set { this.shadowedImageList = value; if (this.UseSubItemCheckBoxes) this.SetupSubItemCheckBoxes(); this.SetupBaseImageList(); } } private ImageList shadowedImageList; /// /// 返回小图像列表中图像的大小或合理的默认值 /// [Browsable(false)] public virtual Size SmallImageSize { get { return this.BaseSmallImageList == null ? new Size(16, 16) : this.BaseSmallImageList.ImageSize; } } /// /// 当列表视图分组时,项是否应该按主列排序?如果为False,则项目将按分组时的同一列进行排序。 /// [Category("ObjectListView"), Description("当列表视图分组时,项是否应该按主列排序?如果为False,则项目将按分组时的同一列进行排序。"), DefaultValue(true)] public virtual bool SortGroupItemsByPrimaryColumn { get { return this.sortGroupItemsByPrimaryColumn; } set { this.sortGroupItemsByPrimaryColumn = value; } } private bool sortGroupItemsByPrimaryColumn = true; /// ///当列表视图分组时,一个组的末尾和下一个组的开头之间应该有多少像素 /// [Category("ObjectListView"), Description("当列表视图分组时,一个组的末尾和下一个组的开头之间应该有多少像素"), DefaultValue(0)] public virtual int SpaceBetweenGroups { get { return this.spaceBetweenGroups; } set { if (this.spaceBetweenGroups == value) return; this.spaceBetweenGroups = value; this.SetGroupSpacing(); } } private int spaceBetweenGroups; private void SetGroupSpacing() { if (!this.IsHandleCreated) return; NativeMethods.LVGROUPMETRICS metrics = new NativeMethods.LVGROUPMETRICS(); metrics.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUPMETRICS))); metrics.mask = (uint)GroupMetricsMask.LVGMF_BORDERSIZE; metrics.Bottom = (uint)this.SpaceBetweenGroups; NativeMethods.SetGroupMetrics(this, metrics); } /// /// Should the sort column show a slight tinge? /// [Category("ObjectListView"), Description("Should the sort column show a slight tinting?"), DefaultValue(false)] public virtual bool TintSortColumn { get { return this.tintSortColumn; } set { this.tintSortColumn = value; if (value && this.PrimarySortColumn != null) this.SelectedColumn = this.PrimarySortColumn; else this.SelectedColumn = null; } } private bool tintSortColumn; /// /// 获取或设置是否使用三态复选框? /// /// /// 如果为True,用户可以选择第三种状态(通常是不确定的)。 /// 否则,用户在选中和取消选中之间交替单击。 /// 当此设置为False时,CheckStateGetter仍可以返回不确定。 /// [Category("ObjectListView"), Description("Should the primary column have a checkbox that behaves as a tri-state checkbox?"), DefaultValue(false)] public virtual bool TriStateCheckBoxes { get { return triStateCheckBoxes; } set { triStateCheckBoxes = value; if (value && !this.CheckBoxes) this.CheckBoxes = true; this.InitializeStateImageList(); } } private bool triStateCheckBoxes; /// /// 获取或设置列表顶部项目的索引 /// /// /// /// 此属性仅在列表视图处于详细信息视图中且不显示组时才起作用。 /// /// /// 显示组时不起作用的原因是,当启用组时,无论滚动位置如何, /// Windows msg LVM_GETTOPINDEX总是返回0。 /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual int TopItemIndex { get { if (this.View == View.Details && this.IsHandleCreated) return NativeMethods.GetTopIndex(this); return -1; } set { int newTopIndex = Math.Min(value, this.GetItemCount() - 1); if (this.View != View.Details || newTopIndex < 0) return; try { this.TopItem = this.Items[newTopIndex]; // Setting the TopItem sometimes gives off by one errors, // that (bizarrely) are correct on a second attempt if (this.TopItem != null && this.TopItem.Index != newTopIndex) this.TopItem = this.GetItem(newTopIndex); } catch (NullReferenceException) { // There is a bug in the .NET code where setting the TopItem // will sometimes throw null reference exceptions // There is nothing we can do to get around it. } } } /// /// 获取或设置将鼠标移到标题上是否会触发CellOver事件。 /// 默认为true。 /// /// /// 将鼠标移动到标题上之前不会触发CellOver事件,因为Header被视为单独的控件。 /// 如果此行为更改导致您的应用程序出现问题,请将其设置为False。 /// 如果您想知道鼠标何时在标题上移动,请将此属性设置为true(默认值)。 /// [Category("ObjectListView"), Description("获取或设置将鼠标移到标题上是否会触发CellOver事件。"), DefaultValue(true)] public bool TriggerCellOverEventsWhenOverHeader { get { return triggerCellOverEventsWhenOverHeader; } set { triggerCellOverEventsWhenOverHeader = value; } } private bool triggerCellOverEventsWhenOverHeader = true; /// /// 通过拖动分隔线调整列的大小时,是否应在每次鼠标移动时调整填充空格的列的大小? /// 如果为False,则释放鼠标时将更新填充栏。 /// /// /// /// 如果空间填充列位于正在调整大小的列的左侧,这看起来会很奇怪:该列的右边缘将被拖动,但由于空间填充列正在收缩,其左边缘将会移动。 /// /// 这是合乎逻辑的行为--只是看起来不对。 /// /// /// 考虑到上述行为,如果空间填充列不是最右边的列,最好关闭该属性。 /// [Category("ObjectListView"), Description("通过拖动分隔线调整列的大小时,是否应在每次鼠标移动时调整填充空格的列的大小"), DefaultValue(true)] public virtual bool UpdateSpaceFillingColumnsWhenDraggingColumnDivider { get { return updateSpaceFillingColumnsWhenDraggingColumnDivider; } set { updateSpaceFillingColumnsWhenDraggingColumnDivider = value; } } private bool updateSpaceFillingColumnsWhenDraggingColumnDivider = true; /// /// 当控件没有焦点时,应该使用什么颜色作为选定行的背景 /// [Category("ObjectListView"), Description("当控件没有焦点时,应该使用什么颜色作为选定行的背景"), DefaultValue(typeof(Color), "")] public virtual Color UnfocusedSelectedBackColor { get { return this.unfocusedSelectedBackColor; } set { this.unfocusedSelectedBackColor = value; } } private Color unfocusedSelectedBackColor = Color.Empty; /// /// 当控件没有焦点或合理的默认值时,返回应用于选定行的背景的颜色 /// [Browsable(false)] public virtual Color UnfocusedSelectedBackColorOrDefault { get { return this.UnfocusedSelectedBackColor.IsEmpty ? SystemColors.Control : this.UnfocusedSelectedBackColor; } } /// ///当控件没有焦点时,应该使用什么颜色作为选定行的字体颜色。 /// [Category("ObjectListView"), Description("当控件没有焦点时,应该使用什么颜色作为选定行的字体颜色。"), DefaultValue(typeof(Color), "")] public virtual Color UnfocusedSelectedForeColor { get { return this.unfocusedSelectedForeColor; } set { this.unfocusedSelectedForeColor = value; } } private Color unfocusedSelectedForeColor = Color.Empty; /// /// 当控件没有焦点或合理的默认值时,返回应用于选定行的字体颜色 /// [Browsable(false)] public virtual Color UnfocusedSelectedForeColorOrDefault { get { return this.UnfocusedSelectedForeColor.IsEmpty ? SystemColors.ControlText : this.UnfocusedSelectedForeColor; } } /// /// 获取或设置列表是否每隔一行提供不同的背景色?默认值为false。 /// /// 交替行的颜色由AlternateRowBackColor提供。 /// 在.NET中,列表视图在非整行选择模式下有一个“功能”,在这种模式下,所选行不会使用正确的背景色绘制。 [Category("ObjectListView"), Description("获取或设置列表是否每隔一行提供不同的背景色?默认值为false。"), DefaultValue(false)] public virtual bool UseAlternatingBackColors { get { return useAlternatingBackColors; } set { useAlternatingBackColors = value; } } private bool useAlternatingBackColors; /// /// 是否应该为控件中的每个单元格调用FormatCell事件? /// /// /// 在许多情况下,不执行单元格级格式设置。 /// 如果除非需要,ObjectListView不会为每个单元格触发Format Cell事件,则它的运行速度会稍快一些。 /// 因此,默认情况下,它不会为每个单元格引发事件。 /// /// ObjectListView*确实*在每次重新生成行时引发FormatRow事件。各行可以决定是否对行中的每个单元格引发FormatCell事件。 /// /// /// 无论此设置如何,只有当ObjectListView处于详细视图中时才会引发FormatCell事件。 /// [Category("ObjectListView"), Description("是否应该为控件中的每个单元格调用FormatCell事件"), DefaultValue(false)] public bool UseCellFormatEvents { get { return useCellFormatEvents; } set { useCellFormatEvents = value; } } private bool useCellFormatEvents; /// /// 所选行是否应使用非标准前景色和背景色绘制? /// /// V2.9不再需要此属性 [Category("ObjectListView"), Description("所选行是否应使用非标准前景色和背景色绘制"), DefaultValue(false)] public bool UseCustomSelectionColors { get { return false; } // ReSharper disable once ValueParameterNotUsed set { } } /// /// 获取或设置此ObjectListView是否将使用与Vista资源管理器相同的热项和选择机制。 /// /// /// /// 此属性有许多缺陷: /// /// 这仅适用于Vista及更高版本 /// 它不能很好地与AlternateRowBackColors配合使用。 /// 它不能很好地与HotItemStyles配合使用。 /// 如果FullRowSelect为false,这看起来有点傻。 /// 当列表是所有者绘制的时,它根本不起作用(因为所有的绘制都是由渲染器完成的)。 /// 因此,它不能与TreeListView一起工作,因为它们“必须”是所有者绘制的。你仍然可以设置它,但它不会生效。 /// ///但如果你一定要看起来像Vista/Win7,这是你的权限。如果这项设置搞砸了其他事情,请不要抱怨。 /// /// /// 当此属性设置为True时,ObjectListView将不是所有者描述的。 /// 这将禁用ObjectListView的许多漂亮的基于绘图的功能。 /// /// [Category("ObjectListView"), Description("获取或设置此ObjectListView是否将使用与Vista资源管理器相同的热项和选择机制"), DefaultValue(false)] public bool UseExplorerTheme { get { return useExplorerTheme; } set { useExplorerTheme = value; if (this.Created) NativeMethods.SetWindowTheme(this.Handle, value ? "explorer" : "", null); this.OwnerDraw = !value; } } private bool useExplorerTheme; /// /// 获取或设置列表是否应启用筛选 /// [Category("ObjectListView"), Description("获取或设置列表是否应启用筛选"), DefaultValue(false)] virtual public bool UseFiltering { get { return useFiltering; } set { if (useFiltering == value) return; useFiltering = value; this.UpdateFiltering(); } } private bool useFiltering; /// /// 获取或设置列表是否应将指示符放入列的标题中,以表明它正在筛选该列 /// /// 如果将其设置为true,HeaderUsesThemes将自动设置为false,因为我们只能在不使用主题标题时绘制筛选器指示器。 [Category("ObjectListView"), Description("获取或设置列表是否应将指示符放入列的标题中,以表明它正在筛选该列"), DefaultValue(false)] virtual public bool UseFilterIndicator { get { return useFilterIndicator; } set { if (this.useFilterIndicator == value) return; useFilterIndicator = value; if (this.useFilterIndicator) this.HeaderUsesThemes = false; this.Invalidate(); } } private bool useFilterIndicator; /// /// 鼠标下方的控件(复选框或按钮)是否应该被绘制为“Hot” /// /// /// 如果为False,则当鼠标悬停在控件上时,控件的绘制方式不会有所不同。 /// /// 如果为False,且UseHotItem和UseHyperLinks为False,则ObjectListView可以跳过鼠标移动的某些处理。这使得鼠标移动处理几乎不使用CPU。 /// /// [Category("ObjectListView"), Description("鼠标下方的控件(复选框或按钮)是否应该被绘制为“Hot”"), DefaultValue(true)] public bool UseHotControls { get { return this.useHotControls; } set { this.useHotControls = value; } } private bool useHotControls = true; /// /// 光标下的项是否应以特殊方式设置格式? /// [Category("ObjectListView"), Description("光标下的项是否应以特殊方式设置格式"), DefaultValue(false)] public bool UseHotItem { get { return this.useHotItem; } set { this.useHotItem = value; if (value) this.AddOverlay(this.HotItemStyleOrDefault.Overlay); else this.RemoveOverlay(this.HotItemStyleOrDefault.Overlay); } } private bool useHotItem; /// /// 获取或设置此列表视图是否应在单元格中显示超链接。 /// [Category("ObjectListView"), Description("获取或设置此列表视图是否应在单元格中显示超链接"), DefaultValue(false)] public bool UseHyperlinks { get { return this.useHyperlinks; } set { this.useHyperlinks = value; if (value && this.HyperlinkStyle == null) this.HyperlinkStyle = new HyperlinkStyle(); } } private bool useHyperlinks; /// /// 此控件是否应显示覆盖(Overlays) /// /// 覆盖(Overlays)在默认情况下是启用的,只有在它们在您的开发环境中造成问题时才需要禁用。 [Category("ObjectListView"), Description("Should this control show overlays"), DefaultValue(true)] public bool UseOverlays { get { return this.useOverlays; } set { this.useOverlays = value; } } private bool useOverlays = true; /// /// 是否应将此控件配置为在子项上显示复选框? /// /// 如果将其设置为True,则将为该控件提供一个SmallImageList(如果它还没有一个SmallImageList)。 /// 此外,如果它是一个虚拟列表,它将被设置为所有者描述的,因为虚拟列表不能在没有所有者描述的情况下绘制复选框。 [Category("ObjectListView"), Description("是否应将此控件配置为在子项上显示复选框?"), DefaultValue(false)] public bool UseSubItemCheckBoxes { get { return this.useSubItemCheckBoxes; } set { this.useSubItemCheckBoxes = value; if (value) this.SetupSubItemCheckBoxes(); } } private bool useSubItemCheckBoxes; /// ///获取或设置ObjectListView是否将使用类似Vista的半透明选择机制。 /// /// /// /// 与UseExplorerTheme不同的是,这个类似Vista的方案可以在XP上运行,并且既适用于所有者描述的列表,也适用于非所有者描述的列表。 /// /// ///这将替换任何已安装的SelectedRowDecory。 /// /// /// 如果您不喜欢用于选择的颜色,请忽略此属性, /// 只需创建您自己的RowBorderDecory并将其分配给SelectedRowDecory, /// 就像此属性设置器所做的那样。 /// /// [Category("ObjectListView"), Description("获取或设置ObjectListView是否将使用类似Vista的半透明选择机制"), DefaultValue(false)] public bool UseTranslucentSelection { get { return useTranslucentSelection; } set { useTranslucentSelection = value; if (value) { RowBorderDecoration rbd = new RowBorderDecoration(); rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); rbd.FillBrush = new SolidBrush(Color.FromArgb(48, 163, 217, 225)); rbd.BoundsPadding = new Size(0, 0); rbd.CornerRounding = 6.0f; this.SelectedRowDecoration = rbd; } else this.SelectedRowDecoration = null; } } private bool useTranslucentSelection; /// /// 获取或设置ObjectListView是否将使用类似于Vista的半透明热行突出显示机制。 /// /// /// /// 设置此项将替换任何已安装的HotItemStyle。 /// /// /// 如果您不喜欢热项目使用的颜色,请忽略此属性, /// 只需创建您自己的HotItemStyle,填充所需的值, /// 并将其分配给HotItemStyle属性,就像此属性设置器所做的那样。 /// /// [Category("ObjectListView"), Description("获取或设置ObjectListView是否将使用类似于Vista的半透明热行突出显示机制。"), DefaultValue(false)] public bool UseTranslucentHotItem { get { return useTranslucentHotItem; } set { useTranslucentHotItem = value; if (value) { RowBorderDecoration rbd = new RowBorderDecoration(); rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); rbd.BoundsPadding = new Size(0, 0); rbd.CornerRounding = 6.0f; rbd.FillGradientFrom = Color.FromArgb(0, 255, 255, 255); rbd.FillGradientTo = Color.FromArgb(64, 183, 237, 240); HotItemStyle his = new HotItemStyle(); his.Decoration = rbd; this.HotItemStyle = his; } else this.HotItemStyle = null; this.UseHotItem = value; } } private bool useTranslucentHotItem; /// /// 获取/设置此列表视图正在使用的视图样式 /// /// 切换到平铺(Tile)或详细信息(Details)视图会安装适合该视图的列。令人困惑的是,在平铺视图中,每列都显示为一行信息。 [Category("Appearance"), Description("获取/设置此列表视图正在使用的视图样式"), DefaultValue(null)] new public View View { get { return base.View; } set { if (base.View == value) return; if (this.Frozen) { base.View = value; this.SetupBaseImageList(); } else { this.Freeze(); if (value == View.Tile) this.CalculateReasonableTileSize(); base.View = value; this.SetupBaseImageList(); this.Unfreeze(); } } } #endregion #region Callbacks /// /// This delegate fetches the checkedness of an object as a boolean only. /// /// Use this if you never want to worry about the /// Indeterminate state (which is fairly common). /// /// This is a convenience wrapper around the CheckStateGetter property. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual BooleanCheckStateGetterDelegate BooleanCheckStateGetter { set { if (value == null) this.CheckStateGetter = null; else this.CheckStateGetter = delegate(Object x) { return value(x) ? CheckState.Checked : CheckState.Unchecked; }; } } /// /// This delegate sets the checkedness of an object as a boolean only. It must return /// true or false indicating if the object was checked or not. /// /// Use this if you never want to worry about the /// Indeterminate state (which is fairly common). /// /// This is a convenience wrapper around the CheckStatePutter property. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual BooleanCheckStatePutterDelegate BooleanCheckStatePutter { set { if (value == null) this.CheckStatePutter = null; else this.CheckStatePutter = delegate(Object x, CheckState state) { bool isChecked = (state == CheckState.Checked); return value(x, isChecked) ? CheckState.Checked : CheckState.Unchecked; }; } } /// /// Gets whether or not this listview is capabale of showing groups /// [Browsable(false)] public virtual bool CanShowGroups { get { return true; } } /// /// 获取或设置ObjectListView是否可以依赖正在引发的Application.Idle事件。 /// /// 在某些主机环境中(例如,在VisualStudio和Office中作为扩展运行时),从不引发Application.Idle事件。 /// 如果不会引发Idle事件,则将其设置为False,ObjectListView将自行引发这些事件。 /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual bool CanUseApplicationIdle { get { return this.canUseApplicationIdle; } set { this.canUseApplicationIdle = value; } } private bool canUseApplicationIdle = true; /// /// This delegate fetches the renderer for a particular cell. /// /// /// /// If this returns null (or is not installed), the renderer for the column will be used. /// If the column renderer is null, then will be used. /// /// /// This is called every time any cell is drawn. It must be efficient! /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual CellRendererGetterDelegate CellRendererGetter { get { return this.cellRendererGetter; } set { this.cellRendererGetter = value; } } private CellRendererGetterDelegate cellRendererGetter; /// /// This delegate is called when the list wants to show a tooltip for a particular cell. /// The delegate should return the text to display, or null to use the default behavior /// (which is to show the full text of truncated cell values). /// /// /// Displaying the full text of truncated cell values only work for FullRowSelect listviews. /// This is MS's behavior, not mine. Don't complain to me :) /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual CellToolTipGetterDelegate CellToolTipGetter { get { return cellToolTipGetter; } set { cellToolTipGetter = value; } } private CellToolTipGetterDelegate cellToolTipGetter; /// /// The name of the property (or field) that holds whether or not a model is checked. /// /// /// The property be modifiable. It must have a return type of bool (or of bool? if /// TriStateCheckBoxes is true). /// Setting this property replaces any CheckStateGetter or CheckStatePutter that have been installed. /// Conversely, later setting the CheckStateGetter or CheckStatePutter properties will take precedence /// over the behavior of this property. /// [Category("ObjectListView"), Description("The name of the property or field that holds the 'checkedness' of the model"), DefaultValue(null)] public virtual string CheckedAspectName { get { return checkedAspectName; } set { checkedAspectName = value; if (String.IsNullOrEmpty(checkedAspectName)) { this.checkedAspectMunger = null; this.CheckStateGetter = null; this.CheckStatePutter = null; } else { this.checkedAspectMunger = new Munger(checkedAspectName); this.CheckStateGetter = delegate(Object modelObject) { bool? result = this.checkedAspectMunger.GetValue(modelObject) as bool?; if (result.HasValue) return result.Value ? CheckState.Checked : CheckState.Unchecked; return this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; }; this.CheckStatePutter = delegate(Object modelObject, CheckState newValue) { if (this.TriStateCheckBoxes && newValue == CheckState.Indeterminate) this.checkedAspectMunger.PutValue(modelObject, null); else this.checkedAspectMunger.PutValue(modelObject, newValue == CheckState.Checked); return this.CheckStateGetter(modelObject); }; } } } private string checkedAspectName; private Munger checkedAspectMunger; /// /// This delegate will be called whenever the ObjectListView needs to know the check state /// of the row associated with a given model object. /// /// /// .NET has no support for indeterminate values, but as of v2.0, this class allows /// indeterminate values. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual CheckStateGetterDelegate CheckStateGetter { get { return checkStateGetter; } set { checkStateGetter = value; } } private CheckStateGetterDelegate checkStateGetter; /// /// This delegate will be called whenever the user tries to change the check state of a row. /// The delegate should return the state that was actually set, which may be different /// to the state given. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual CheckStatePutterDelegate CheckStatePutter { get { return checkStatePutter; } set { checkStatePutter = value; } } private CheckStatePutterDelegate checkStatePutter; /// /// This delegate can be used to sort the table in a custom fasion. /// /// /// /// The delegate must install a ListViewItemSorter on the ObjectListView. /// Installing the ItemSorter does the actual work of sorting the ListViewItems. /// See ColumnComparer in the code for an example of what an ItemSorter has to do. /// /// /// Do not install a CustomSorter on a VirtualObjectListView. Override the SortObjects() /// method of the IVirtualListDataSource instead. /// /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual SortDelegate CustomSorter { get { return customSorter; } set { customSorter = value; } } private SortDelegate customSorter; /// /// This delegate is called when the list wants to show a tooltip for a particular header. /// The delegate should return the text to display, or null to use the default behavior /// (which is to not show any tooltip). /// /// /// Installing a HeaderToolTipGetter takes precedence over any text in OLVColumn.ToolTipText. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual HeaderToolTipGetterDelegate HeaderToolTipGetter { get { return headerToolTipGetter; } set { headerToolTipGetter = value; } } private HeaderToolTipGetterDelegate headerToolTipGetter; /// /// This delegate can be used to format a OLVListItem before it is added to the control. /// /// /// The model object for the row can be found through the RowObject property of the OLVListItem object. /// All subitems normally have the same style as list item, so setting the forecolor on one /// subitem changes the forecolor of all subitems. /// To allow subitems to have different attributes, do this: /// myListViewItem.UseItemStyleForSubItems = false;. /// /// If UseAlternatingBackColors is true, the backcolor of the listitem will be calculated /// by the control and cannot be controlled by the RowFormatter delegate. /// In general, trying to use a RowFormatter /// when UseAlternatingBackColors is true does not work well. /// As it says in the summary, this is called before the item is added to the control. /// Many properties of the OLVListItem itself are not available at that point, including: /// Index, Selected, Focused, Bounds, Checked, DisplayIndex. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual RowFormatterDelegate RowFormatter { get { return rowFormatter; } set { rowFormatter = value; } } private RowFormatterDelegate rowFormatter; #endregion #region List commands /// /// Add the given model object to this control. /// /// The model object to be displayed /// See AddObjects() for more details public virtual void AddObject(object modelObject) { if (this.InvokeRequired) this.Invoke((MethodInvoker)delegate() { this.AddObject(modelObject); }); else this.AddObjects(new object[] { modelObject }); } /// /// Add the given collection of model objects to this control. /// /// A collection of model objects /// /// The added objects will appear in their correct sort position, if sorting /// is active (i.e. if PrimarySortColumn is not null). Otherwise, they will appear at the end of the list. /// No check is performed to see if any of the objects are already in the ListView. /// Null objects are silently ignored. /// public virtual void AddObjects(ICollection modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate() { this.AddObjects(modelObjects); }); return; } this.InsertObjects(ObjectListView.EnumerableCount(this.Objects), modelObjects); this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); } /// /// Resize the columns to the maximum of the header width and the data. /// public virtual void AutoResizeColumns() { foreach (OLVColumn c in this.Columns) { this.AutoResizeColumn(c.Index, ColumnHeaderAutoResizeStyle.HeaderSize); } } /// /// Set up any automatically initialized column widths (columns that /// have a width of 0 or -1 will be resized to the width of their /// contents or header respectively). /// /// /// Obviously, this will only work once. Once it runs, the columns widths will /// be changed to something else (other than 0 or -1), so it wont do anything the /// second time through. Use to force all columns /// to change their size. /// public virtual void AutoSizeColumns() { // If we are supposed to resize to content, but if there is no content, // resize to the header size instead. ColumnHeaderAutoResizeStyle resizeToContentStyle = this.GetItemCount() == 0 ? ColumnHeaderAutoResizeStyle.HeaderSize : ColumnHeaderAutoResizeStyle.ColumnContent; foreach (ColumnHeader column in this.Columns) { switch (column.Width) { case 0: this.AutoResizeColumn(column.Index, resizeToContentStyle); break; case -1: this.AutoResizeColumn(column.Index, ColumnHeaderAutoResizeStyle.HeaderSize); break; } } } /// /// Organise the view items into groups, based on the last sort column or the first column /// if there is no last sort column /// public virtual void BuildGroups() { this.BuildGroups(this.PrimarySortColumn, this.PrimarySortOrder == SortOrder.None ? SortOrder.Ascending : this.PrimarySortOrder); } /// /// Organise the view items into groups, based on the given column /// /// /// /// If the AlwaysGroupByColumn property is not null, /// the list view items will be organisd by that column, /// and the 'column' parameter will be ignored. /// /// This method triggers sorting events: BeforeSorting and AfterSorting. /// /// The column whose values should be used for sorting. /// public virtual void BuildGroups(OLVColumn column, SortOrder order) { // Sanity if (this.GetItemCount() == 0 || this.Columns.Count == 0) return; BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(column, order); this.OnBeforeSorting(args); if (args.Canceled) return; this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, args.ColumnToSort, args.SortOrder, args.SecondaryColumnToSort, args.SecondarySortOrder); this.OnAfterSorting(new AfterSortingEventArgs(args)); } private BeforeSortingEventArgs BuildBeforeSortingEventArgs(OLVColumn column, SortOrder order) { OLVColumn groupBy = this.AlwaysGroupByColumn ?? column ?? this.GetColumn(0); SortOrder groupByOrder = this.AlwaysGroupBySortOrder; if (order == SortOrder.None) { order = this.Sorting; if (order == SortOrder.None) order = SortOrder.Ascending; } if (groupByOrder == SortOrder.None) groupByOrder = order; BeforeSortingEventArgs args = new BeforeSortingEventArgs( groupBy, groupByOrder, column, order, this.SecondarySortColumn ?? this.GetColumn(0), this.SecondarySortOrder == SortOrder.None ? order : this.SecondarySortOrder); if (column != null) args.Canceled = !column.Sortable; return args; } /// /// Organise the view items into groups, based on the given columns /// /// What column will be used for grouping /// What ordering will be used for groups /// The column whose values should be used for sorting. Cannot be null /// The order in which the values from column will be sorted /// When the values from 'column' are equal, use the values provided by this column /// How will the secondary values be sorted /// This method does not trigger sorting events. Use BuildGroups() to do that public virtual void BuildGroups(OLVColumn groupByColumn, SortOrder groupByOrder, OLVColumn column, SortOrder order, OLVColumn secondaryColumn, SortOrder secondaryOrder) { // Sanity checks if (groupByColumn == null) return; // Getting the Count forces any internal cache of the ListView to be flushed. Without // this, iterating over the Items will not work correctly if the ListView handle // has not yet been created. #pragma warning disable 168 // ReSharper disable once UnusedVariable int dummy = this.Items.Count; #pragma warning restore 168 // Collect all the information that governs the creation of groups GroupingParameters parms = this.CollectGroupingParameters(groupByColumn, groupByOrder, column, order, secondaryColumn, secondaryOrder); // Trigger an event to let the world create groups if they want CreateGroupsEventArgs args = new CreateGroupsEventArgs(parms); if (parms.GroupByColumn != null) args.Canceled = !parms.GroupByColumn.Groupable; this.OnBeforeCreatingGroups(args); if (args.Canceled) return; // If the event didn't create them for us, use our default strategy if (args.Groups == null) args.Groups = this.MakeGroups(parms); // Give the world a chance to munge the groups before they are created this.OnAboutToCreateGroups(args); if (args.Canceled) return; // Create the groups now this.OLVGroups = args.Groups; this.CreateGroups(args.Groups); // Tell the world that new groups have been created this.OnAfterCreatingGroups(args); lastGroupingParameters = args.Parameters; } private GroupingParameters lastGroupingParameters; /// /// Collect and return all the variables that influence the creation of groups /// /// protected virtual GroupingParameters CollectGroupingParameters(OLVColumn groupByColumn, SortOrder groupByOrder, OLVColumn sortByColumn, SortOrder sortByOrder, OLVColumn secondaryColumn, SortOrder secondaryOrder) { // If the user tries to group by a non-groupable column, keep the current group by // settings, but use the non-groupable column for sorting if (!groupByColumn.Groupable && lastGroupingParameters != null) { sortByColumn = groupByColumn; sortByOrder = groupByOrder; groupByColumn = lastGroupingParameters.GroupByColumn; groupByOrder = lastGroupingParameters.GroupByOrder; } string titleFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountFormatOrDefault : null; string titleSingularFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountSingularFormatOrDefault : null; GroupingParameters parms = new GroupingParameters(this, groupByColumn, groupByOrder, sortByColumn, sortByOrder, secondaryColumn, secondaryOrder, titleFormat, titleSingularFormat, this.SortGroupItemsByPrimaryColumn); return parms; } /// /// Make a list of groups that should be shown according to the given parameters /// /// /// The list of groups to be created /// This should not change the state of the control. It is possible that the /// groups created will not be used. They may simply be discarded. protected virtual IList MakeGroups(GroupingParameters parms) { // There is a lot of overlap between this method and FastListGroupingStrategy.MakeGroups() // Any changes made here may need to be reflected there // Separate the list view items into groups, using the group key as the descrimanent NullableDictionary> map = new NullableDictionary>(); foreach (OLVListItem olvi in parms.ListView.Items) { object key = parms.GroupByColumn.GetGroupKey(olvi.RowObject); if (!map.ContainsKey(key)) map[key] = new List(); map[key].Add(olvi); } // Sort the items within each group (unless specifically turned off) OLVColumn sortColumn = parms.SortItemsByPrimaryColumn ? parms.ListView.GetColumn(0) : parms.PrimarySort; if (sortColumn != null && parms.PrimarySortOrder != SortOrder.None) { IComparer itemSorter = parms.ItemComparer ?? new ColumnComparer(sortColumn, parms.PrimarySortOrder, parms.SecondarySort, parms.SecondarySortOrder); foreach (object key in map.Keys) { map[key].Sort(itemSorter); } } // Make a list of the required groups List groups = new List(); foreach (object key in map.Keys) { string title = parms.GroupByColumn.ConvertGroupKeyToTitle(key); if (!String.IsNullOrEmpty(parms.TitleFormat)) { int count = map[key].Count; string format = (count == 1 ? parms.TitleSingularFormat : parms.TitleFormat); try { title = String.Format(format, title, count); } catch (FormatException) { title = "Invalid group format: " + format; } } OLVGroup lvg = new OLVGroup(title); lvg.Collapsible = this.HasCollapsibleGroups; lvg.Key = key; lvg.SortValue = key as IComparable; lvg.Items = map[key]; if (parms.GroupByColumn.GroupFormatter != null) parms.GroupByColumn.GroupFormatter(lvg, parms); groups.Add(lvg); } // Sort the groups if (parms.GroupByOrder != SortOrder.None) groups.Sort(parms.GroupComparer ?? new OLVGroupComparer(parms.GroupByOrder)); return groups; } /// /// Build/rebuild all the list view items in the list, preserving as much state as is possible /// public virtual void BuildList() { if (this.InvokeRequired) this.Invoke(new MethodInvoker(this.BuildList)); else this.BuildList(true); } /// /// Build/rebuild all the list view items in the list /// /// If this is true, the control will try to preserve the selection, /// focused item, and the scroll position (see Remarks) /// /// /// /// Use this method in situations were the contents of the list is basically the same /// as previously. /// /// public virtual void BuildList(bool shouldPreserveState) { if (this.Frozen) return; Stopwatch sw = Stopwatch.StartNew(); this.ApplyExtendedStyles(); this.ClearHotItem(); int previousTopIndex = this.TopItemIndex; Point currentScrollPosition = this.LowLevelScrollPosition; IList previousSelection = new ArrayList(); Object previousFocus = null; if (shouldPreserveState && this.objects != null) { previousSelection = this.SelectedObjects; OLVListItem focusedItem = this.FocusedItem as OLVListItem; if (focusedItem != null) previousFocus = focusedItem.RowObject; } IEnumerable objectsToDisplay = this.FilteredObjects; this.BeginUpdate(); try { this.Items.Clear(); this.ListViewItemSorter = null; if (objectsToDisplay != null) { // Build a list of all our items and then display them. (Building // a list and then doing one AddRange is about 10-15% faster than individual adds) List itemList = new List(); // use ListViewItem to avoid co-variant conversion foreach (object rowObject in objectsToDisplay) { OLVListItem lvi = new OLVListItem(rowObject); this.FillInValues(lvi, rowObject); itemList.Add(lvi); } this.Items.AddRange(itemList.ToArray()); this.Sort(); if (shouldPreserveState) { this.SelectedObjects = previousSelection; this.FocusedItem = this.ModelToItem(previousFocus); } } } finally { this.EndUpdate(); } this.RefreshHotItem(); // We can only restore the scroll position after the EndUpdate() because // of caching that the ListView does internally during a BeginUpdate/EndUpdate pair. if (shouldPreserveState) { // Restore the scroll position. TopItemIndex is best, but doesn't work // when the control is grouped. if (this.ShowGroups) this.LowLevelScroll(currentScrollPosition.X, currentScrollPosition.Y); else this.TopItemIndex = previousTopIndex; } // System.Diagnostics.Debug.WriteLine(String.Format("PERF - Building list for {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); } /// /// Clear any cached info this list may have been using /// public virtual void ClearCachedInfo() { // ObjectListView doesn't currently cache information but subclass do (or might) } /// /// Apply all required extended styles to our control. /// /// /// /// Whenever .NET code sets an extended style, it erases all other extended styles /// that it doesn't use. So, we have to explicit reapply the styles that we have /// added. /// /// /// Normally, we would override CreateParms property and update /// the ExStyle member, but ListView seems to ignore all ExStyles that /// it doesn't already know about. Worse, when we set the LVS_EX_HEADERINALLVIEWS /// value, bad things happen (the control crashes!). /// /// protected virtual void ApplyExtendedStyles() { const int LVS_EX_SUBITEMIMAGES = 0x00000002; //const int LVS_EX_TRANSPARENTBKGND = 0x00400000; const int LVS_EX_HEADERINALLVIEWS = 0x02000000; const int STYLE_MASK = LVS_EX_SUBITEMIMAGES | LVS_EX_HEADERINALLVIEWS; int style = 0; if (this.ShowImagesOnSubItems && !this.VirtualMode) style ^= LVS_EX_SUBITEMIMAGES; if (this.ShowHeaderInAllViews) style ^= LVS_EX_HEADERINALLVIEWS; NativeMethods.SetExtendedStyle(this, style, STYLE_MASK); } /// /// Give the listview a reasonable size of its tiles, based on the number of lines of /// information that each tile is going to display. /// public virtual void CalculateReasonableTileSize() { if (this.Columns.Count <= 0) return; List columns = this.AllColumns.FindAll(delegate(OLVColumn x) { return (x.Index == 0) || x.IsTileViewColumn; }); int imageHeight = (this.LargeImageList == null ? 16 : this.LargeImageList.ImageSize.Height); int dataHeight = (this.Font.Height + 1) * columns.Count; int tileWidth = (this.TileSize.Width == 0 ? 200 : this.TileSize.Width); int tileHeight = Math.Max(this.TileSize.Height, Math.Max(imageHeight, dataHeight)); this.TileSize = new Size(tileWidth, tileHeight); } /// /// Rebuild this list for the given view /// /// public virtual void ChangeToFilteredColumns(View view) { // Store the state this.SuspendSelectionEvents(); IList previousSelection = this.SelectedObjects; int previousTopIndex = this.TopItemIndex; this.Freeze(); this.Clear(); List columns = this.GetFilteredColumns(view); if (view == View.Details || this.ShowHeaderInAllViews) { // Make sure all columns have a reasonable LastDisplayIndex for (int index = 0; index < columns.Count; index++) { if (columns[index].LastDisplayIndex == -1) columns[index].LastDisplayIndex = index; } // ListView will ignore DisplayIndex FOR ALL COLUMNS if there are any errors, // e.g. duplicates (two columns with the same DisplayIndex) or gaps. // LastDisplayIndex isn't guaranteed to be unique, so we just sort the columns by // the last position they were displayed and use that to generate a sequence // we can use for the DisplayIndex values. List columnsInDisplayOrder = new List(columns); columnsInDisplayOrder.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); int i = 0; foreach (OLVColumn col in columnsInDisplayOrder) col.DisplayIndex = i++; } // ReSharper disable once CoVariantArrayConversion this.Columns.AddRange(columns.ToArray()); if (view == View.Details || this.ShowHeaderInAllViews) this.ShowSortIndicator(); this.UpdateFiltering(); this.Unfreeze(); // Restore the state this.SelectedObjects = previousSelection; this.TopItemIndex = previousTopIndex; this.ResumeSelectionEvents(); } /// /// Remove all items from this list /// /// This method can safely be called from background threads. public virtual void ClearObjects() { if (this.InvokeRequired) this.Invoke(new MethodInvoker(this.ClearObjects)); else this.SetObjects(null); } /// /// Reset the memory of which URLs have been visited /// public virtual void ClearUrlVisited() { this.visitedUrlMap = new Dictionary(); } /// /// Copy a text and html representation of the selected rows onto the clipboard. /// /// Be careful when using this with virtual lists. If the user has selected /// 10,000,000 rows, this method will faithfully try to copy all of them to the clipboard. /// From the user's point of view, your program will appear to have hung. public virtual void CopySelectionToClipboard() { IList selection = this.SelectedObjects; if (selection.Count == 0) return; // Use the DragSource object to create the data object, if so configured. // This relies on the assumption that DragSource will handle the selected objects only. // It is legal for StartDrag to return null. object data = null; if (this.CopySelectionOnControlCUsesDragSource && this.DragSource != null) data = this.DragSource.StartDrag(this, MouseButtons.Left, this.ModelToItem(selection[0])); Clipboard.SetDataObject(data ?? new OLVDataObject(this, selection)); } /// /// Copy a text and html representation of the given objects onto the clipboard. /// public virtual void CopyObjectsToClipboard(IList objectsToCopy) { if (objectsToCopy.Count == 0) return; // We don't know where these objects came from, so we can't use the DragSource to create // the data object, like we do with CopySelectionToClipboard() above. OLVDataObject dataObject = new OLVDataObject(this, objectsToCopy); dataObject.CreateTextFormats(); Clipboard.SetDataObject(dataObject); } /// /// Return a html representation of the given objects /// public virtual string ObjectsToHtml(IList objectsToConvert) { if (objectsToConvert.Count == 0) return String.Empty; OLVExporter exporter = new OLVExporter(this, objectsToConvert); return exporter.ExportTo(OLVExporter.ExportFormat.HTML); } /// /// Deselect all rows in the listview /// public virtual void DeselectAll() { NativeMethods.DeselectAllItems(this); } /// /// Return the ListViewItem that appears immediately after the given item. /// If the given item is null, the first item in the list will be returned. /// Return null if the given item is the last item. /// /// The item that is before the item that is returned, or null /// A ListViewItem public virtual OLVListItem GetNextItem(OLVListItem itemToFind) { if (this.ShowGroups) { bool isFound = (itemToFind == null); foreach (ListViewGroup group in this.Groups) { foreach (OLVListItem olvi in group.Items) { if (isFound) return olvi; isFound = (itemToFind == olvi); } } return null; } if (this.GetItemCount() == 0) return null; if (itemToFind == null) return this.GetItem(0); if (itemToFind.Index == this.GetItemCount() - 1) return null; return this.GetItem(itemToFind.Index + 1); } /// /// Return the last item in the order they are shown to the user. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// public virtual OLVListItem GetLastItemInDisplayOrder() { if (!this.ShowGroups) return this.GetItem(this.GetItemCount() - 1); if (this.Groups.Count > 0) { ListViewGroup lastGroup = this.Groups[this.Groups.Count - 1]; if (lastGroup.Items.Count > 0) return (OLVListItem)lastGroup.Items[lastGroup.Items.Count - 1]; } return null; } /// /// Return the n'th item (0-based) in the order they are shown to the user. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// /// public virtual OLVListItem GetNthItemInDisplayOrder(int n) { if (!this.ShowGroups || this.Groups.Count == 0) return this.GetItem(n); foreach (ListViewGroup group in this.Groups) { if (n < group.Items.Count) return (OLVListItem)group.Items[n]; n -= group.Items.Count; } return null; } /// /// Return the display index of the given listviewitem index. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// /// public virtual int GetDisplayOrderOfItemIndex(int itemIndex) { if (!this.ShowGroups || this.Groups.Count == 0) return itemIndex; // TODO: This could be optimized int i = 0; foreach (ListViewGroup lvg in this.Groups) { foreach (ListViewItem lvi in lvg.Items) { if (lvi.Index == itemIndex) return i; i++; } } return -1; } /// /// Return the ListViewItem that appears immediately before the given item. /// If the given item is null, the last item in the list will be returned. /// Return null if the given item is the first item. /// /// The item that is before the item that is returned /// A ListViewItem public virtual OLVListItem GetPreviousItem(OLVListItem itemToFind) { if (this.ShowGroups) { OLVListItem previousItem = null; foreach (ListViewGroup group in this.Groups) { foreach (OLVListItem lvi in group.Items) { if (lvi == itemToFind) return previousItem; previousItem = lvi; } } return itemToFind == null ? previousItem : null; } if (this.GetItemCount() == 0) return null; if (itemToFind == null) return this.GetItem(this.GetItemCount() - 1); if (itemToFind.Index == 0) return null; return this.GetItem(itemToFind.Index - 1); } /// /// Insert the given collection of objects before the given position /// /// Where to insert the objects /// The objects to be inserted /// /// /// This operation only makes sense of non-sorted, non-grouped /// lists, since any subsequent sort/group operation will rearrange /// the list. /// /// This method only works on ObjectListViews and FastObjectListViews. /// public virtual void InsertObjects(int index, ICollection modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate() { this.InsertObjects(index, modelObjects); }); return; } if (modelObjects == null) return; this.BeginUpdate(); try { // Give the world a chance to cancel or change the added objects ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); this.OnItemsAdding(args); if (args.Canceled) return; modelObjects = args.ObjectsToAdd; this.TakeOwnershipOfObjects(); ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); // If we are filtering the list, there is no way to efficiently // insert the objects, so just put them into our collection and rebuild. if (this.IsFiltering) { index = Math.Max(0, Math.Min(index, ourObjects.Count)); ourObjects.InsertRange(index, modelObjects); this.BuildList(true); } else { this.ListViewItemSorter = null; index = Math.Max(0, Math.Min(index, this.GetItemCount())); int i = index; foreach (object modelObject in modelObjects) { if (modelObject != null) { ourObjects.Insert(i, modelObject); OLVListItem lvi = new OLVListItem(modelObject); this.FillInValues(lvi, modelObject); this.Items.Insert(i, lvi); i++; } } for (i = index; i < this.GetItemCount(); i++) { OLVListItem lvi = this.GetItem(i); this.SetSubItemImages(lvi.Index, lvi); } this.PostProcessRows(); } // Tell the world that the list has changed this.SubscribeNotifications(modelObjects); this.OnItemsChanged(new ItemsChangedEventArgs()); } finally { this.EndUpdate(); } } /// /// Return true if the row representing the given model is selected /// /// The model object to look for /// Is the row selected public bool IsSelected(object model) { OLVListItem item = this.ModelToItem(model); return item != null && item.Selected; } /// /// Has the given URL been visited? /// /// The string to be consider /// Has it been visited public virtual bool IsUrlVisited(string url) { return this.visitedUrlMap.ContainsKey(url); } /// /// Scroll the ListView by the given deltas. /// /// Horizontal delta /// Vertical delta public void LowLevelScroll(int dx, int dy) { NativeMethods.Scroll(this, dx, dy); } /// /// Return a point that represents the current horizontal and vertical scroll positions /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Point LowLevelScrollPosition { get { return new Point(NativeMethods.GetScrollPosition(this, true), NativeMethods.GetScrollPosition(this, false)); } } /// /// Remember that the given URL has been visited /// /// The url to be remembered /// This does not cause the control be redrawn public virtual void MarkUrlVisited(string url) { this.visitedUrlMap[url] = true; } /// /// Move the given collection of objects to the given index. /// /// This operation only makes sense on non-grouped ObjectListViews. /// /// public virtual void MoveObjects(int index, ICollection modelObjects) { // We are going to remove all the given objects from our list // and then insert them at the given location this.TakeOwnershipOfObjects(); ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); List indicesToRemove = new List(); foreach (object modelObject in modelObjects) { if (modelObject != null) { int i = this.IndexOf(modelObject); if (i >= 0) { indicesToRemove.Add(i); ourObjects.Remove(modelObject); if (i <= index) index--; } } } // Remove the objects in reverse order so earlier // deletes don't change the index of later ones indicesToRemove.Sort(); indicesToRemove.Reverse(); try { this.BeginUpdate(); foreach (int i in indicesToRemove) { this.Items.RemoveAt(i); } this.InsertObjects(index, modelObjects); } finally { this.EndUpdate(); } } /// /// Calculate what item is under the given point? /// /// /// /// new public ListViewHitTestInfo HitTest(int x, int y) { // Everything costs something. Playing with the layout of the header can cause problems // with the hit testing. If the header shrinks, the underlying control can throw a tantrum. try { return base.HitTest(x, y); } catch (ArgumentOutOfRangeException) { return new ListViewHitTestInfo(null, null, ListViewHitTestLocations.None); } } /// /// Perform a hit test using the Windows control's SUBITEMHITTEST message. /// This provides information about group hits that the standard ListView.HitTest() does not. /// /// /// /// protected OlvListViewHitTestInfo LowLevelHitTest(int x, int y) { // If it's not even in the control, don't bother with anything else if (!this.ClientRectangle.Contains(x, y)) return new OlvListViewHitTestInfo(null, null, 0, null, 0); // Is the point over the header? OlvListViewHitTestInfo.HeaderHitTestInfo headerHitTestInfo = this.HeaderControl.HitTest(x, y); if (headerHitTestInfo != null) return new OlvListViewHitTestInfo(this, headerHitTestInfo.ColumnIndex, headerHitTestInfo.IsOverCheckBox, headerHitTestInfo.OverDividerIndex); // Call the native hit test method, which is a little confusing. NativeMethods.LVHITTESTINFO lParam = new NativeMethods.LVHITTESTINFO(); lParam.pt_x = x; lParam.pt_y = y; int index = NativeMethods.HitTest(this, ref lParam); // Setup the various values we need to make our hit test structure bool isGroupHit = (lParam.flags & (int)HitTestLocationEx.LVHT_EX_GROUP) != 0; OLVListItem hitItem = isGroupHit || index == -1 ? null : this.GetItem(index); OLVListSubItem subItem = (this.View == View.Details && hitItem != null) ? hitItem.GetSubItem(lParam.iSubItem) : null; // Figure out which group is involved in the hit test. This is a little complicated: // If the list is virtual: // - the returned value is list view item index // - iGroup is the *index* of the hit group. // If the list is not virtual: // - iGroup is always -1. // - if the point is over a group, the returned value is the *id* of the hit group. // - if the point is not over a group, the returned value is list view item index. OLVGroup group = null; if (this.ShowGroups && this.OLVGroups != null) { if (this.VirtualMode) { group = lParam.iGroup >= 0 && lParam.iGroup < this.OLVGroups.Count ? this.OLVGroups[lParam.iGroup] : null; } else { if (isGroupHit) { foreach (OLVGroup olvGroup in this.OLVGroups) { if (olvGroup.GroupId == index) { group = olvGroup; break; } } } } } OlvListViewHitTestInfo olvListViewHitTest = new OlvListViewHitTestInfo(hitItem, subItem, lParam.flags, group, lParam.iSubItem); // System.Diagnostics.Debug.WriteLine(String.Format("HitTest({0}, {1})=>{2}", x, y, olvListViewHitTest)); return olvListViewHitTest; } /// /// What is under the given point? This takes the various parts of a cell into accout, including /// any custom parts that a custom renderer might use /// /// /// /// An information block about what is under the point public virtual OlvListViewHitTestInfo OlvHitTest(int x, int y) { OlvListViewHitTestInfo hti = this.LowLevelHitTest(x, y); // There is a bug/"feature" of the ListView concerning hit testing. // If FullRowSelect is false and the point is over cell 0 but not on // the text or icon, HitTest will not register a hit. We could turn // FullRowSelect on, do the HitTest, and then turn it off again, but // toggling FullRowSelect in that way messes up the tooltip in the // underlying control. So we have to find another way. // // It's too hard to try to write the hit test from scratch. Grouping (for // example) makes it just too complicated. So, we have to use HitTest // but try to get around its limits. // // First step is to determine if the point was within column 0. // If it was, then we only have to determine if there is an actual row // under the point. If there is, then we know that the point is over cell 0. // So we try a Battleship-style approach: is there a subcell to the right // of cell 0? This will return a false negative if column 0 is the rightmost column, // so we also check for a subcell to the left. But if only column 0 is visible, // then that will fail too, so we check for something at the very left of the // control. // // This will still fail under pathological conditions. If column 0 fills // the whole listview and no part of the text column 0 is visible // (because it is horizontally scrolled offscreen), then the hit test will fail. // Are we in the buggy context? Details view, not full row select, and // failing to find anything if (hti.Item == null && !this.FullRowSelect && this.View == View.Details) { // Is the point within the column 0? If it is, maybe it should have been a hit. // Let's test slightly to the right and then to left of column 0. Hopefully one // of those will hit a subitem Point sides = NativeMethods.GetScrolledColumnSides(this, 0); if (x >= sides.X && x <= sides.Y) { // We look for: // - any subitem to the right of cell 0? // - any subitem to the left of cell 0? // - cell 0 at the left edge of the screen hti = this.LowLevelHitTest(sides.Y + 4, y); if (hti.Item == null) hti = this.LowLevelHitTest(sides.X - 4, y); if (hti.Item == null) hti = this.LowLevelHitTest(4, y); if (hti.Item != null) { // We hit something! So, the original point must have been in cell 0 hti.ColumnIndex = 0; hti.SubItem = hti.Item.GetSubItem(0); hti.Location = ListViewHitTestLocations.None; hti.HitTestLocation = HitTestLocation.InCell; } } } if (this.OwnerDraw) this.CalculateOwnerDrawnHitTest(hti, x, y); else this.CalculateStandardHitTest(hti, x, y); return hti; } /// /// Perform a hit test when the control is not owner drawn /// /// /// /// protected virtual void CalculateStandardHitTest(OlvListViewHitTestInfo hti, int x, int y) { // Standard hit test works fine for the primary column if (this.View != View.Details || hti.ColumnIndex == 0 || hti.SubItem == null || hti.Column == null) return; Rectangle cellBounds = hti.SubItem.Bounds; bool hasImage = (this.GetActualImageIndex(hti.SubItem.ImageSelector) != -1); // Unless we say otherwise, it was an general incell hit hti.HitTestLocation = HitTestLocation.InCell; // Check if the point is over where an image should be. // If there is a checkbox or image there, tag it and exit. Rectangle r = cellBounds; r.Width = this.SmallImageSize.Width; if (r.Contains(x, y)) { if (hti.Column.CheckBoxes) { hti.HitTestLocation = HitTestLocation.CheckBox; return; } if (hasImage) { hti.HitTestLocation = HitTestLocation.Image; return; } } // Figure out where the text actually is and if the point is in it // The standard HitTest assumes that any point inside a subitem is // a hit on Text -- which is clearly not true. Rectangle textBounds = cellBounds; textBounds.X += 4; if (hasImage) textBounds.X += this.SmallImageSize.Width; Size proposedSize = new Size(textBounds.Width, textBounds.Height); Size textSize = TextRenderer.MeasureText(hti.SubItem.Text, this.Font, proposedSize, TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); textBounds.Width = textSize.Width; switch (hti.Column.TextAlign) { case HorizontalAlignment.Center: textBounds.X += (cellBounds.Right - cellBounds.Left - textSize.Width) / 2; break; case HorizontalAlignment.Right: textBounds.X = cellBounds.Right - textSize.Width; break; } if (textBounds.Contains(x, y)) { hti.HitTestLocation = HitTestLocation.Text; } } /// /// Perform a hit test when the control is owner drawn. This hands off responsibility /// to the renderer. /// /// /// /// protected virtual void CalculateOwnerDrawnHitTest(OlvListViewHitTestInfo hti, int x, int y) { // If the click wasn't on an item, give up if (hti.Item == null) return; // If the list is showing column, but they clicked outside the columns, also give up if (this.View == View.Details && hti.Column == null) return; // Which renderer was responsible for drawing that point IRenderer renderer = this.View == View.Details ? this.GetCellRenderer(hti.RowObject, hti.Column) : this.ItemRenderer; // We can't decide who was responsible. Give up if (renderer == null) return; // Ask the responsible renderer what is at that point renderer.HitTest(hti, x, y); } /// /// Pause (or unpause) all animations in the list /// /// true to pause, false to unpause public virtual void PauseAnimations(bool isPause) { for (int i = 0; i < this.Columns.Count; i++) { OLVColumn col = this.GetColumn(i); ImageRenderer renderer = col.Renderer as ImageRenderer; if (renderer != null) renderer.Paused = isPause; } } /// /// Rebuild the columns based upon its current view and column visibility settings /// public virtual void RebuildColumns() { this.ChangeToFilteredColumns(this.View); } /// /// Remove the given model object from the ListView /// /// The model to be removed /// See RemoveObjects() for more details /// This method is thread-safe. /// public virtual void RemoveObject(object modelObject) { if (this.InvokeRequired) this.Invoke((MethodInvoker)delegate() { this.RemoveObject(modelObject); }); else this.RemoveObjects(new object[] { modelObject }); } /// /// Remove all of the given objects from the control. /// /// Collection of objects to be removed /// /// Nulls and model objects that are not in the ListView are silently ignored. /// This method is thread-safe. /// public virtual void RemoveObjects(ICollection modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate() { this.RemoveObjects(modelObjects); }); return; } if (modelObjects == null) return; this.BeginUpdate(); try { // Give the world a chance to cancel or change the added objects ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); this.OnItemsRemoving(args); if (args.Canceled) return; modelObjects = args.ObjectsToRemove; this.TakeOwnershipOfObjects(); ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); foreach (object modelObject in modelObjects) { if (modelObject != null) { // ReSharper disable PossibleMultipleEnumeration int i = ourObjects.IndexOf(modelObject); if (i >= 0) ourObjects.RemoveAt(i); // ReSharper restore PossibleMultipleEnumeration i = this.IndexOf(modelObject); if (i >= 0) this.Items.RemoveAt(i); } } this.PostProcessRows(); // Tell the world that the list has changed this.UnsubscribeNotifications(modelObjects); this.OnItemsChanged(new ItemsChangedEventArgs()); } finally { this.EndUpdate(); } } /// /// Select all rows in the listview /// public virtual void SelectAll() { NativeMethods.SelectAllItems(this); } /// /// Set the given image to be fixed in the bottom right of the list view. /// This image will not scroll when the list view scrolls. /// /// /// /// This method uses ListView's native ability to display a background image. /// It has a few limitations: /// /// /// It doesn't work well with owner drawn mode. In owner drawn mode, each cell draws itself, /// including its background, which covers the background image. /// It doesn't look very good when grid lines are enabled, since the grid lines are drawn over the image. /// It does not work at all on XP. /// Obviously, it doesn't look good when alternate row background colors are enabled. /// /// /// If you can live with these limitations, native watermarks are quite neat. They are true backgrounds, not /// translucent overlays like the OverlayImage uses. They also have the decided advantage over overlays in that /// they work correctly even in MDI applications. /// /// Setting this clears any background image. /// /// The image to be drawn. If null, any existing image will be removed. public void SetNativeBackgroundWatermark(Image image) { NativeMethods.SetBackgroundImage(this, image, true, false, 0, 0); } /// /// Set the given image to be background of the ListView so that it appears at the given /// percentage offsets within the list. /// /// /// This has the same limitations as described in . Make sure those limitations /// are understood before using the method. /// This is very similar to setting the property of the standard .NET ListView, except that the standard /// BackgroundImage does not handle images with transparent areas properly -- it renders transparent areas as black. This /// method does not have that problem. /// Setting this clears any background watermark. /// /// The image to be drawn. If null, any existing image will be removed. /// The horizontal percentage where the image will be placed. 0 is absolute left, 100 is absolute right. /// The vertical percentage where the image will be placed. public void SetNativeBackgroundImage(Image image, int xOffset, int yOffset) { NativeMethods.SetBackgroundImage(this, image, false, false, xOffset, yOffset); } /// /// Set the given image to be the tiled background of the ListView. /// /// /// This has the same limitations as described in and . /// Make sure those limitations /// are understood before using the method. /// /// The image to be drawn. If null, any existing image will be removed. public void SetNativeBackgroundTiledImage(Image image) { NativeMethods.SetBackgroundImage(this, image, false, true, 0, 0); } /// /// Set the collection of objects that will be shown in this list view. /// /// This method can safely be called from background threads. /// The list is updated immediately /// The objects to be displayed public virtual void SetObjects(IEnumerable collection) { this.SetObjects(collection, false); } /// /// Set the collection of objects that will be shown in this list view. /// /// This method can safely be called from background threads. /// The list is updated immediately /// The objects to be displayed /// Should the state of the list be preserved as far as is possible. public virtual void SetObjects(IEnumerable collection, bool preserveState) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); return; } // Give the world a chance to cancel or change the assigned collection ItemsChangingEventArgs args = new ItemsChangingEventArgs(this.objects, collection); this.OnItemsChanging(args); if (args.Canceled) return; collection = args.NewObjects; // If we own the current list and they change to another list, we don't own it anymore if (this.isOwnerOfObjects && !ReferenceEquals(this.objects, collection)) this.isOwnerOfObjects = false; this.objects = collection; this.BuildList(preserveState); // Tell the world that the list has changed this.UpdateNotificationSubscriptions(this.objects); this.OnItemsChanged(new ItemsChangedEventArgs()); } /// /// Update the given model object into the ListView. The model will be added if it doesn't already exist. /// /// The model to be updated /// /// /// See for more details /// /// This method is thread-safe. /// This method will cause the list to be resorted. /// This method only works on ObjectListViews and FastObjectListViews. /// public virtual void UpdateObject(object modelObject) { if (this.InvokeRequired) this.Invoke((MethodInvoker)delegate() { this.UpdateObject(modelObject); }); else this.UpdateObjects(new object[] { modelObject }); } /// /// Update the pre-existing models that are equal to the given objects. If any of the model doesn't /// already exist in the control, they will be added. /// /// Collection of objects to be updated/added /// /// This method will cause the list to be resorted. /// Nulls are silently ignored. /// This method is thread-safe. /// This method only works on ObjectListViews and FastObjectListViews. /// public virtual void UpdateObjects(ICollection modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate() { this.UpdateObjects(modelObjects); }); return; } if (modelObjects == null || modelObjects.Count == 0) return; this.BeginUpdate(); try { this.UnsubscribeNotifications(modelObjects); ArrayList objectsToAdd = new ArrayList(); this.TakeOwnershipOfObjects(); ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); foreach (object modelObject in modelObjects) { if (modelObject != null) { int i = ourObjects.IndexOf(modelObject); if (i < 0) objectsToAdd.Add(modelObject); else { ourObjects[i] = modelObject; OLVListItem olvi = this.ModelToItem(modelObject); if (olvi != null) { olvi.RowObject = modelObject; this.RefreshItem(olvi); } } } } this.PostProcessRows(); this.AddObjects(objectsToAdd); // Tell the world that the list has changed this.SubscribeNotifications(modelObjects); this.OnItemsChanged(new ItemsChangedEventArgs()); } finally { this.EndUpdate(); } } /// /// Change any subscriptions to INotifyPropertyChanged events on our current /// model objects so that we no longer listen for events on the old models /// and do listen for events on the given collection. /// /// This does nothing if UseNotifyPropertyChanged is false. /// protected virtual void UpdateNotificationSubscriptions(IEnumerable collection) { if (!this.UseNotifyPropertyChanged) return; // We could calculate a symmetric difference between the old models and the new models // except that we don't have the previous models at this point. this.UnsubscribeNotifications(null); this.SubscribeNotifications(collection ?? this.Objects); } /// /// Gets or sets whether or not ObjectListView should subscribe to INotifyPropertyChanged /// events on the model objects that it is given. /// /// /// /// This should be set before calling SetObjects(). If you set this to false, /// ObjectListView will unsubscribe to all current model objects. /// /// If you set this to true on a virtual list, the ObjectListView will /// walk all the objects in the list trying to subscribe to change notifications. /// If you have 10,000,000 items in your virtual list, this may take some time. /// [Category("ObjectListView"), Description("Should ObjectListView listen for property changed events on the model objects?"), DefaultValue(false)] public bool UseNotifyPropertyChanged { get { return this.useNotifyPropertyChanged; } set { if (this.useNotifyPropertyChanged == value) return; this.useNotifyPropertyChanged = value; if (value) this.SubscribeNotifications(this.Objects); else this.UnsubscribeNotifications(null); } } private bool useNotifyPropertyChanged; /// /// Subscribe to INotifyPropertyChanges on the given collection of objects. /// /// protected void SubscribeNotifications(IEnumerable models) { if (!this.UseNotifyPropertyChanged || models == null) return; foreach (object x in models) { INotifyPropertyChanged notifier = x as INotifyPropertyChanged; if (notifier != null && !subscribedModels.ContainsKey(notifier)) { notifier.PropertyChanged += HandleModelOnPropertyChanged; subscribedModels[notifier] = notifier; } } } /// /// Unsubscribe from INotifyPropertyChanges on the given collection of objects. /// If the given collection is null, unsubscribe from all current subscriptions /// /// protected void UnsubscribeNotifications(IEnumerable models) { if (models == null) { foreach (INotifyPropertyChanged notifier in this.subscribedModels.Keys) { notifier.PropertyChanged -= HandleModelOnPropertyChanged; } subscribedModels = new Hashtable(); } else { foreach (object x in models) { INotifyPropertyChanged notifier = x as INotifyPropertyChanged; if (notifier != null) { notifier.PropertyChanged -= HandleModelOnPropertyChanged; subscribedModels.Remove(notifier); } } } } private void HandleModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) { // System.Diagnostics.Debug.WriteLine(String.Format("PropertyChanged: '{0}' on '{1}", propertyChangedEventArgs.PropertyName, sender)); this.RefreshObject(sender); } private Hashtable subscribedModels = new Hashtable(); #endregion #region Save/Restore State /// /// Return a byte array that represents the current state of the ObjectListView, such /// that the state can be restored by RestoreState() /// /// /// The state of an ObjectListView includes the attributes that the user can modify: /// /// current view (i.e. Details, Tile, Large Icon...) /// sort column and direction /// column order /// column widths /// column visibility /// /// /// /// It does not include selection or the scroll position. /// /// /// A byte array representing the state of the ObjectListView public virtual byte[] SaveState() { ObjectListViewState olvState = new ObjectListViewState(); olvState.VersionNumber = 1; olvState.NumberOfColumns = this.AllColumns.Count; olvState.CurrentView = this.View; // If we have a sort column, it is possible that it is not currently being shown, in which // case, it's Index will be -1. So we calculate its index directly. Technically, the sort // column does not even have to a member of AllColumns, in which case IndexOf will return -1, // which is works fine since we have no way of restoring such a column anyway. if (this.PrimarySortColumn != null) olvState.SortColumn = this.AllColumns.IndexOf(this.PrimarySortColumn); olvState.LastSortOrder = this.PrimarySortOrder; olvState.IsShowingGroups = this.ShowGroups; if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) this.RememberDisplayIndicies(); foreach (OLVColumn column in this.AllColumns) { olvState.ColumnIsVisible.Add(column.IsVisible); olvState.ColumnDisplayIndicies.Add(column.LastDisplayIndex); olvState.ColumnWidths.Add(column.Width); } // Now that we have stored our state, convert it to a byte array using (MemoryStream ms = new MemoryStream()) { BinaryFormatter serializer = new BinaryFormatter(); serializer.AssemblyFormat = FormatterAssemblyStyle.Simple; serializer.Serialize(ms, olvState); return ms.ToArray(); } } /// /// Restore the state of the control from the given string, which must have been /// produced by SaveState() /// /// A byte array returned from SaveState() /// Returns true if the state was restored public virtual bool RestoreState(byte[] state) { using (MemoryStream ms = new MemoryStream(state)) { BinaryFormatter deserializer = new BinaryFormatter(); ObjectListViewState olvState; try { olvState = deserializer.Deserialize(ms) as ObjectListViewState; } catch (System.Runtime.Serialization.SerializationException) { return false; } // The number of columns has changed. We have no way to match old // columns to the new ones, so we just give up. if (olvState == null || olvState.NumberOfColumns != this.AllColumns.Count) return false; if (olvState.SortColumn == -1) { this.PrimarySortColumn = null; this.PrimarySortOrder = SortOrder.None; } else { this.PrimarySortColumn = this.AllColumns[olvState.SortColumn]; this.PrimarySortOrder = olvState.LastSortOrder; } for (int i = 0; i < olvState.NumberOfColumns; i++) { OLVColumn column = this.AllColumns[i]; column.Width = (int)olvState.ColumnWidths[i]; column.IsVisible = (bool)olvState.ColumnIsVisible[i]; column.LastDisplayIndex = (int)olvState.ColumnDisplayIndicies[i]; } // ReSharper disable RedundantCheckBeforeAssignment if (olvState.IsShowingGroups != this.ShowGroups) // ReSharper restore RedundantCheckBeforeAssignment this.ShowGroups = olvState.IsShowingGroups; if (this.View == olvState.CurrentView) this.RebuildColumns(); else this.View = olvState.CurrentView; } return true; } /// /// Instances of this class are used to store the state of an ObjectListView. /// [Serializable] internal class ObjectListViewState { // ReSharper disable NotAccessedField.Global public int VersionNumber = 1; // ReSharper restore NotAccessedField.Global public int NumberOfColumns = 1; public View CurrentView; public int SortColumn = -1; public bool IsShowingGroups; public SortOrder LastSortOrder = SortOrder.None; // ReSharper disable FieldCanBeMadeReadOnly.Global public ArrayList ColumnIsVisible = new ArrayList(); public ArrayList ColumnDisplayIndicies = new ArrayList(); public ArrayList ColumnWidths = new ArrayList(); // ReSharper restore FieldCanBeMadeReadOnly.Global } #endregion #region Event handlers /// /// The application is idle. Trigger a SelectionChanged event. /// /// /// protected virtual void HandleApplicationIdle(object sender, EventArgs e) { // Remove the handler before triggering the event Application.Idle -= new EventHandler(HandleApplicationIdle); this.hasIdleHandler = false; this.OnSelectionChanged(new EventArgs()); } /// /// The application is idle. Handle the column resizing event. /// /// /// protected virtual void HandleApplicationIdleResizeColumns(object sender, EventArgs e) { // Remove the handler before triggering the event Application.Idle -= new EventHandler(this.HandleApplicationIdleResizeColumns); this.hasResizeColumnsHandler = false; this.ResizeFreeSpaceFillingColumns(); } /// /// Handle the BeginScroll listview notification /// /// /// True if the event was completely handled protected virtual bool HandleBeginScroll(ref Message m) { //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); NativeMethods.NMLVSCROLL nmlvscroll = (NativeMethods.NMLVSCROLL)m.GetLParam(typeof(NativeMethods.NMLVSCROLL)); if (nmlvscroll.dx != 0) { int scrollPositionH = NativeMethods.GetScrollPosition(this, true); ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionH - nmlvscroll.dx, scrollPositionH, ScrollOrientation.HorizontalScroll); this.OnScroll(args); // Force any empty list msg to redraw when the list is scrolled horizontally if (this.GetItemCount() == 0) this.Invalidate(); } if (nmlvscroll.dy != 0) { int scrollPositionV = NativeMethods.GetScrollPosition(this, false); ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionV - nmlvscroll.dy, scrollPositionV, ScrollOrientation.VerticalScroll); this.OnScroll(args); } return false; } /// /// Handle the EndScroll listview notification /// /// /// True if the event was completely handled protected virtual bool HandleEndScroll(ref Message m) { //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); // There is a bug in ListView under XP that causes the gridlines to be incorrectly scrolled // when the left button is clicked to scroll. This is supposedly documented at // KB 813791, but I couldn't find it anywhere. You can follow this thread to see the discussion // http://www.ureader.com/msg/1484143.aspx if (!ObjectListView.IsVistaOrLater && ObjectListView.IsLeftMouseDown && this.GridLines) { this.Invalidate(); this.Update(); } return false; } /// /// Handle the LinkClick listview notification /// /// /// True if the event was completely handled protected virtual bool HandleLinkClick(ref Message m) { //System.Diagnostics.Debug.WriteLine("HandleLinkClick"); NativeMethods.NMLVLINK nmlvlink = (NativeMethods.NMLVLINK)m.GetLParam(typeof(NativeMethods.NMLVLINK)); // Find the group that was clicked and trigger an event foreach (OLVGroup x in this.OLVGroups) { if (x.GroupId == nmlvlink.iSubItem) { this.OnGroupTaskClicked(new GroupTaskClickedEventArgs(x)); return true; } } return false; } /// /// The cell tooltip control wants information about the tool tip that it should show. /// /// /// protected virtual void HandleCellToolTipShowing(object sender, ToolTipShowingEventArgs e) { this.BuildCellEvent(e, this.PointToClient(Cursor.Position)); if (e.Item != null) { e.Text = this.GetCellToolTip(e.ColumnIndex, e.RowIndex); this.OnCellToolTip(e); } } /// /// Allow the HeaderControl to call back into HandleHeaderToolTipShowing without making that method public /// /// /// internal void HeaderToolTipShowingCallback(object sender, ToolTipShowingEventArgs e) { this.HandleHeaderToolTipShowing(sender, e); } /// /// The header tooltip control wants information about the tool tip that it should show. /// /// /// protected virtual void HandleHeaderToolTipShowing(object sender, ToolTipShowingEventArgs e) { e.ColumnIndex = this.HeaderControl.ColumnIndexUnderCursor; if (e.ColumnIndex < 0) return; e.RowIndex = -1; e.Model = null; e.Column = this.GetColumn(e.ColumnIndex); e.Text = this.GetHeaderToolTip(e.ColumnIndex); e.ListView = this; this.OnHeaderToolTip(e); } /// /// Event handler for the column click event /// protected virtual void HandleColumnClick(object sender, ColumnClickEventArgs e) { if (!this.PossibleFinishCellEditing()) return; // Toggle the sorting direction on successive clicks on the same column if (this.PrimarySortColumn != null && e.Column == this.PrimarySortColumn.Index) this.PrimarySortOrder = (this.PrimarySortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending); else this.PrimarySortOrder = SortOrder.Ascending; this.BeginUpdate(); try { this.Sort(e.Column); } finally { this.EndUpdate(); } } #endregion #region Low level Windows Message handling /// /// Override the basic message pump for this control /// /// protected override void WndProc(ref Message m) { // System.Diagnostics.Debug.WriteLine(m.Msg); switch (m.Msg) { case 2: // WM_DESTROY if (!this.HandleDestroy(ref m)) base.WndProc(ref m); break; //case 0x14: // WM_ERASEBKGND // Can't do anything here since, when the control is double buffered, anything // done here is immediately over-drawn // break; case 0x0F: // WM_PAINT if (!this.HandlePaint(ref m)) base.WndProc(ref m); break; case 0x46: // WM_WINDOWPOSCHANGING if (this.PossibleFinishCellEditing() && !this.HandleWindowPosChanging(ref m)) base.WndProc(ref m); break; case 0x4E: // WM_NOTIFY if (!this.HandleNotify(ref m)) base.WndProc(ref m); break; case 0x0100: // WM_KEY_DOWN if (!this.HandleKeyDown(ref m)) base.WndProc(ref m); break; case 0x0102: // WM_CHAR if (!this.HandleChar(ref m)) base.WndProc(ref m); break; case 0x0200: // WM_MOUSEMOVE if (!this.HandleMouseMove(ref m)) base.WndProc(ref m); break; case 0x0201: // WM_LBUTTONDOWN if (this.PossibleFinishCellEditing() && !this.HandleLButtonDown(ref m)) base.WndProc(ref m); break; case 0x202: // WM_LBUTTONUP if (this.PossibleFinishCellEditing() && !this.HandleLButtonUp(ref m)) base.WndProc(ref m); break; case 0x0203: // WM_LBUTTONDBLCLK if (this.PossibleFinishCellEditing() && !this.HandleLButtonDoubleClick(ref m)) base.WndProc(ref m); break; case 0x0204: // WM_RBUTTONDOWN if (this.PossibleFinishCellEditing() && !this.HandleRButtonDown(ref m)) base.WndProc(ref m); break; case 0x0206: // WM_RBUTTONDBLCLK if (this.PossibleFinishCellEditing() && !this.HandleRButtonDoubleClick(ref m)) base.WndProc(ref m); break; case 0x204E: // WM_REFLECT_NOTIFY if (!this.HandleReflectNotify(ref m)) base.WndProc(ref m); break; case 0x114: // WM_HSCROLL: case 0x115: // WM_VSCROLL: //System.Diagnostics.Debug.WriteLine("WM_VSCROLL"); if (this.PossibleFinishCellEditing()) base.WndProc(ref m); break; case 0x20A: // WM_MOUSEWHEEL: case 0x20E: // WM_MOUSEHWHEEL: if (this.PossibleFinishCellEditing()) base.WndProc(ref m); break; case 0x7B: // WM_CONTEXTMENU if (!this.HandleContextMenu(ref m)) base.WndProc(ref m); break; case 0x1000 + 18: // LVM_HITTEST: //System.Diagnostics.Debug.WriteLine("LVM_HITTEST"); if (this.skipNextHitTest) { //System.Diagnostics.Debug.WriteLine("SKIPPING LVM_HITTEST"); this.skipNextHitTest = false; } else { base.WndProc(ref m); } break; default: base.WndProc(ref m); break; } } /// /// Handle the search for item m if possible. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleChar(ref Message m) { // Trigger a normal KeyPress event, which listeners can handle if they want. // Handling the event stops ObjectListView's fancy search-by-typing. if (this.ProcessKeyEventArgs(ref m)) return true; const int MILLISECONDS_BETWEEN_KEYPRESSES = 1000; // What character did the user type and was it part of a longer string? char character = (char)m.WParam.ToInt32(); //TODO: Will this work on 64 bit or MBCS? if (character == (char)Keys.Back) { // Backspace forces the next key to be considered the start of a new search this.timeLastCharEvent = 0; return true; } if (System.Environment.TickCount < (this.timeLastCharEvent + MILLISECONDS_BETWEEN_KEYPRESSES)) this.lastSearchString += character; else this.lastSearchString = character.ToString(CultureInfo.InvariantCulture); // If this control is showing checkboxes, we want to ignore single space presses, // since they are used to toggle the selected checkboxes. if (this.CheckBoxes && this.lastSearchString == " ") { this.timeLastCharEvent = 0; return true; } // Where should the search start? int start = 0; ListViewItem focused = this.FocusedItem; if (focused != null) { start = this.GetDisplayOrderOfItemIndex(focused.Index); // If the user presses a single key, we search from after the focused item, // being careful not to march past the end of the list if (this.lastSearchString.Length == 1) { start += 1; if (start == this.GetItemCount()) start = 0; } } // Give the world a chance to fiddle with or completely avoid the searching process BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(this.lastSearchString, start); this.OnBeforeSearching(args); if (args.Canceled) return true; // The parameters of the search may have been changed string searchString = args.StringToFind; start = args.StartSearchFrom; // Do the actual search int found = this.FindMatchingRow(searchString, start, SearchDirectionHint.Down); if (found >= 0) { // Select and focus on the found item this.BeginUpdate(); try { this.SelectedIndices.Clear(); OLVListItem lvi = this.GetNthItemInDisplayOrder(found); if (lvi != null) { if (lvi.Enabled) lvi.Selected = true; lvi.Focused = true; this.EnsureVisible(lvi.Index); } } finally { this.EndUpdate(); } } // Tell the world that a search has occurred AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(searchString, found); this.OnAfterSearching(args2); if (!args2.Handled) { if (found < 0) System.Media.SystemSounds.Beep.Play(); } // When did this event occur? this.timeLastCharEvent = System.Environment.TickCount; return true; } private int timeLastCharEvent; private string lastSearchString; /// /// The user wants to see the context menu. /// /// The windows m /// A bool indicating if this m has been handled /// /// We want to ignore context menu requests that are triggered by right clicks on the header /// protected virtual bool HandleContextMenu(ref Message m) { // Don't try to handle context menu commands at design time. if (this.DesignMode) return false; // If the context menu command was generated by the keyboard, LParam will be -1. // We don't want to process these. if (m.LParam == this.minusOne) return false; // If the context menu came from somewhere other than the header control, // we also don't want to ignore it if (m.WParam != this.HeaderControl.Handle) return false; // OK. Looks like a right click in the header if (!this.PossibleFinishCellEditing()) return true; int columnIndex = this.HeaderControl.ColumnIndexUnderCursor; return this.HandleHeaderRightClick(columnIndex); } readonly IntPtr minusOne = new IntPtr(-1); /// /// Handle the Custom draw series of notifications /// /// The message /// True if the message has been handled protected virtual bool HandleCustomDraw(ref Message m) { const int CDDS_PREPAINT = 1; const int CDDS_POSTPAINT = 2; const int CDDS_PREERASE = 3; const int CDDS_POSTERASE = 4; //const int CDRF_NEWFONT = 2; //const int CDRF_SKIPDEFAULT = 4; const int CDDS_ITEM = 0x00010000; const int CDDS_SUBITEM = 0x00020000; const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT); const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT); const int CDDS_ITEMPREERASE = (CDDS_ITEM | CDDS_PREERASE); const int CDDS_ITEMPOSTERASE = (CDDS_ITEM | CDDS_POSTERASE); const int CDDS_SUBITEMPREPAINT = (CDDS_SUBITEM | CDDS_ITEMPREPAINT); const int CDDS_SUBITEMPOSTPAINT = (CDDS_SUBITEM | CDDS_ITEMPOSTPAINT); const int CDRF_NOTIFYPOSTPAINT = 0x10; //const int CDRF_NOTIFYITEMDRAW = 0x20; //const int CDRF_NOTIFYSUBITEMDRAW = 0x20; // same value as above! const int CDRF_NOTIFYPOSTERASE = 0x40; // There is a bug in owner drawn virtual lists which causes lots of custom draw messages // to be sent to the control *outside* of a WmPaint event. AFAIK, these custom draw events // are spurious and only serve to make the control flicker annoyingly. // So, we ignore messages that are outside of a paint event. if (!this.isInWmPaintEvent) return true; // One more complication! Sometimes with owner drawn virtual lists, the act of drawing // the overlays triggers a second attempt to paint the control -- which makes an annoying // flicker. So, we only do the custom drawing once per WmPaint event. if (!this.shouldDoCustomDrawing) return true; NativeMethods.NMLVCUSTOMDRAW nmcustomdraw = (NativeMethods.NMLVCUSTOMDRAW)m.GetLParam(typeof(NativeMethods.NMLVCUSTOMDRAW)); //System.Diagnostics.Debug.WriteLine(String.Format("cd: {0:x}, {1}, {2}", nmcustomdraw.nmcd.dwDrawStage, nmcustomdraw.dwItemType, nmcustomdraw.nmcd.dwItemSpec)); // Ignore drawing of group items if (nmcustomdraw.dwItemType == 1) { // This is the basis of an idea about how to owner draw group headers //nmcustomdraw.clrText = ColorTranslator.ToWin32(Color.DeepPink); //nmcustomdraw.clrFace = ColorTranslator.ToWin32(Color.DeepPink); //nmcustomdraw.clrTextBk = ColorTranslator.ToWin32(Color.DeepPink); //Marshal.StructureToPtr(nmcustomdraw, m.LParam, false); //using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { // g.DrawRectangle(Pens.Red, Rectangle.FromLTRB(nmcustomdraw.rcText.left, nmcustomdraw.rcText.top, nmcustomdraw.rcText.right, nmcustomdraw.rcText.bottom)); //} //m.Result = (IntPtr)((int)m.Result | CDRF_SKIPDEFAULT); return true; } switch (nmcustomdraw.nmcd.dwDrawStage) { case CDDS_PREPAINT: //System.Diagnostics.Debug.WriteLine("CDDS_PREPAINT"); // Remember which items were drawn during this paint cycle if (this.prePaintLevel == 0) this.drawnItems = new List(); // If there are any items, we have to wait until at least one has been painted // before we draw the overlays. If there aren't any items, there will never be any // item paint events, so we can draw the overlays whenever this.isAfterItemPaint = (this.GetItemCount() == 0); this.prePaintLevel++; base.WndProc(ref m); // Make sure that we get postpaint notifications m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); return true; case CDDS_POSTPAINT: //System.Diagnostics.Debug.WriteLine("CDDS_POSTPAINT"); this.prePaintLevel--; // When in group view, we have two problems. On XP, the control sends // a whole heap of PREPAINT/POSTPAINT messages before drawing any items. // We have to wait until after the first item paint before we draw overlays. // On Vista, we have a different problem. On Vista, the control nests calls // to PREPAINT and POSTPAINT. We only want to draw overlays on the outermost // POSTPAINT. if (this.prePaintLevel == 0 && (this.isMarqueSelecting || this.isAfterItemPaint)) { this.shouldDoCustomDrawing = false; // Draw our overlays after everything has been drawn using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { this.DrawAllDecorations(g, this.drawnItems); } } break; case CDDS_ITEMPREPAINT: //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREPAINT"); // When in group view on XP, the control send a whole heap of PREPAINT/POSTPAINT // messages before drawing any items. // We have to wait until after the first item paint before we draw overlays this.isAfterItemPaint = true; // This scheme of catching custom draw msgs works fine, except // for Tile view. Something in .NET's handling of Tile view causes lots // of invalidates and erases. So, we just ignore completely // .NET's handling of Tile view and let the underlying control // do its stuff. Strangely, if the Tile view is // completely owner drawn, those erasures don't happen. if (this.View == View.Tile) { if (this.OwnerDraw && this.ItemRenderer != null) base.WndProc(ref m); } else { base.WndProc(ref m); } m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); return true; case CDDS_ITEMPOSTPAINT: //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTPAINT"); // Remember which items have been drawn so we can draw any decorations for them // once all other painting is finished if (this.Columns.Count > 0) { OLVListItem olvi = this.GetItem((int)nmcustomdraw.nmcd.dwItemSpec); if (olvi != null) this.drawnItems.Add(olvi); } break; case CDDS_SUBITEMPREPAINT: //System.Diagnostics.Debug.WriteLine(String.Format("CDDS_SUBITEMPREPAINT ({0},{1})", (int)nmcustomdraw.nmcd.dwItemSpec, nmcustomdraw.iSubItem)); // There is a bug in the .NET framework which appears when column 0 of an owner drawn listview // is dragged to another column position. // The bounds calculation always returns the left edge of column 0 as being 0. // The effects of this bug become apparent // when the listview is scrolled horizontally: the control can think that column 0 // is no longer visible (the horizontal scroll position is subtracted from the bounds, giving a // rectangle that is offscreen). In those circumstances, column 0 is not redraw because // the control thinks it is not visible and so does not trigger a DrawSubItem event. // To fix this problem, we have to detected the situation -- owner drawing column 0 in any column except 0 -- // trigger our own DrawSubItem, and then prevent the default processing from occuring. // Are we owner drawing column 0 when it's in any column except 0? if (!this.OwnerDraw) return false; int columnIndex = nmcustomdraw.iSubItem; if (columnIndex != 0) return false; int displayIndex = this.Columns[0].DisplayIndex; if (displayIndex == 0) return false; int rowIndex = (int)nmcustomdraw.nmcd.dwItemSpec; OLVListItem item = this.GetItem(rowIndex); if (item == null) return false; // OK. We have the error condition, so lets do what the .NET framework should do. // Trigger an event to draw column 0 when it is not at display index 0 using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { // Correctly calculate the bounds of cell 0 Rectangle r = item.GetSubItemBounds(0); // We can hardcode "0" here since we know we are only doing this for column 0 DrawListViewSubItemEventArgs args = new DrawListViewSubItemEventArgs(g, r, item, item.SubItems[0], rowIndex, 0, this.Columns[0], (ListViewItemStates)nmcustomdraw.nmcd.uItemState); this.OnDrawSubItem(args); // If the event handler wants to do the default processing (i.e. DrawDefault = true), we are stuck. // There is no way we can force the default drawing because of the bug in .NET we are trying to get around. System.Diagnostics.Trace.Assert(!args.DrawDefault, "Default drawing is impossible in this situation"); } m.Result = (IntPtr)4; return true; case CDDS_SUBITEMPOSTPAINT: //System.Diagnostics.Debug.WriteLine("CDDS_SUBITEMPOSTPAINT"); break; // I have included these stages, but it doesn't seem that they are sent for ListViews. // http://www.tech-archive.net/Archive/VC/microsoft.public.vc.mfc/2006-08/msg00220.html case CDDS_PREERASE: //System.Diagnostics.Debug.WriteLine("CDDS_PREERASE"); break; case CDDS_POSTERASE: //System.Diagnostics.Debug.WriteLine("CDDS_POSTERASE"); break; case CDDS_ITEMPREERASE: //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREERASE"); break; case CDDS_ITEMPOSTERASE: //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTERASE"); break; } return false; } bool isAfterItemPaint; List drawnItems; /// /// Handle the underlying control being destroyed /// /// /// protected virtual bool HandleDestroy(ref Message m) { //System.Diagnostics.Debug.WriteLine(String.Format("WM_DESTROY: Disposing={0}, IsDisposed={1}, VirtualMode={2}", Disposing, IsDisposed, VirtualMode)); // Recreate the header control when the listview control is destroyed this.headerControl = null; // When the underlying control is destroyed, we need to recreate and reconfigure its tooltip if (this.cellToolTip != null) { this.cellToolTip.PushSettings(); this.BeginInvoke((MethodInvoker)delegate { this.UpdateCellToolTipHandle(); this.cellToolTip.PopSettings(); }); } return false; } /// /// Handle the search for item m if possible. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleFindItem(ref Message m) { // NOTE: As far as I can see, this message is never actually sent to the control, making this // method redundant! const int LVFI_STRING = 0x0002; NativeMethods.LVFINDINFO findInfo = (NativeMethods.LVFINDINFO)m.GetLParam(typeof(NativeMethods.LVFINDINFO)); // We can only handle string searches if ((findInfo.flags & LVFI_STRING) != LVFI_STRING) return false; int start = m.WParam.ToInt32(); m.Result = (IntPtr)this.FindMatchingRow(findInfo.psz, start, SearchDirectionHint.Down); return true; } /// /// Find the first row after the given start in which the text value in the /// comparison column begins with the given text. The comparison column is column 0, /// unless IsSearchOnSortColumn is true, in which case the current sort column is used. /// /// The text to be prefix matched /// The index of the first row to consider /// Which direction should be searched? /// The index of the first row that matched, or -1 /// The text comparison is a case-insensitive, prefix match. The search will /// search the every row until a match is found, wrapping at the end if needed. public virtual int FindMatchingRow(string text, int start, SearchDirectionHint direction) { // We also can't do anything if we don't have data int rowCount = this.GetItemCount(); if (rowCount == 0) return -1; // Which column are we going to use for our comparing? OLVColumn column = this.GetColumn(0); if (this.IsSearchOnSortColumn && this.View == View.Details && this.PrimarySortColumn != null) column = this.PrimarySortColumn; // Do two searches if necessary to find a match. The second search is the wrap-around part of searching int i; if (direction == SearchDirectionHint.Down) { i = this.FindMatchInRange(text, start, rowCount - 1, column); if (i == -1 && start > 0) i = this.FindMatchInRange(text, 0, start - 1, column); } else { i = this.FindMatchInRange(text, start, 0, column); if (i == -1 && start != rowCount) i = this.FindMatchInRange(text, rowCount - 1, start + 1, column); } return i; } /// /// Find the first row in the given range of rows that prefix matches the string value of the given column. /// /// /// /// /// /// The index of the matched row, or -1 protected virtual int FindMatchInRange(string text, int first, int last, OLVColumn column) { if (first <= last) { for (int i = first; i <= last; i++) { string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) return i; } } else { for (int i = first; i >= last; i--) { string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) return i; } } return -1; } /// /// Handle the Group Info series of notifications /// /// The message /// True if the message has been handled protected virtual bool HandleGroupInfo(ref Message m) { NativeMethods.NMLVGROUP nmlvgroup = (NativeMethods.NMLVGROUP)m.GetLParam(typeof(NativeMethods.NMLVGROUP)); //System.Diagnostics.Debug.WriteLine(String.Format("group: {0}, old state: {1}, new state: {2}", // nmlvgroup.iGroupId, OLVGroup.StateToString(nmlvgroup.uOldState), OLVGroup.StateToString(nmlvgroup.uNewState))); // Ignore state changes that aren't related to selection, focus or collapsedness const uint INTERESTING_STATES = (uint) (GroupState.LVGS_COLLAPSED | GroupState.LVGS_FOCUSED | GroupState.LVGS_SELECTED); if ((nmlvgroup.uOldState & INTERESTING_STATES) == (nmlvgroup.uNewState & INTERESTING_STATES)) return false; foreach (OLVGroup group in this.OLVGroups) { if (group.GroupId == nmlvgroup.iGroupId) { GroupStateChangedEventArgs args = new GroupStateChangedEventArgs(group, (GroupState)nmlvgroup.uOldState, (GroupState)nmlvgroup.uNewState); this.OnGroupStateChanged(args); break; } } return false; } //private static string StateToString(uint state) //{ // if (state == 0) // return Enum.GetName(typeof(GroupState), 0); // List names = new List(); // foreach (int value in Enum.GetValues(typeof(GroupState))) // { // if (value != 0 && (state & value) == value) // { // names.Add(Enum.GetName(typeof(GroupState), value)); // } // } // return names.Count == 0 ? state.ToString("x") : String.Join("|", names.ToArray()); //} /// /// Handle a key down message /// /// /// True if the msg has been handled protected virtual bool HandleKeyDown(ref Message m) { // If this is a checkbox list, toggle the selected rows when the user presses Space if (this.CheckBoxes && m.WParam.ToInt32() == (int)Keys.Space && this.SelectedIndices.Count > 0) { this.ToggleSelectedRowCheckBoxes(); return true; } // Remember the scroll position so we can decide if the listview has scrolled in the // handling of the event. int scrollPositionH = NativeMethods.GetScrollPosition(this, true); int scrollPositionV = NativeMethods.GetScrollPosition(this, false); base.WndProc(ref m); // It's possible that the processing in base.WndProc has actually destroyed this control if (this.IsDisposed) return true; // If the keydown processing changed the scroll position, trigger a Scroll event int newScrollPositionH = NativeMethods.GetScrollPosition(this, true); int newScrollPositionV = NativeMethods.GetScrollPosition(this, false); if (scrollPositionH != newScrollPositionH) { ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionH, newScrollPositionH, ScrollOrientation.HorizontalScroll); this.OnScroll(args); } if (scrollPositionV != newScrollPositionV) { ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionV, newScrollPositionV, ScrollOrientation.VerticalScroll); this.OnScroll(args); } if (scrollPositionH != newScrollPositionH || scrollPositionV != newScrollPositionV) this.RefreshHotItem(); return true; } /// /// Toggle the checkedness of the selected rows /// /// /// /// Actually, this doesn't actually toggle all rows. It toggles the first row, and /// all other rows get the check state of that first row. This is actually a much /// more useful behaviour. /// /// /// If no rows are selected, this method does nothing. /// /// public void ToggleSelectedRowCheckBoxes() { if (this.SelectedIndices.Count == 0) return; Object primaryModel = this.GetItem(this.SelectedIndices[0]).RowObject; this.ToggleCheckObject(primaryModel); CheckState? state = this.GetCheckState(primaryModel); if (state.HasValue) { foreach (Object x in this.SelectedObjects) this.SetObjectCheckedness(x, state.Value); } } /// /// Catch the Left Button down event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleLButtonDown(ref Message m) { // We have to intercept this low level message rather than the more natural // overridding of OnMouseDown, since ListCtrl's internal mouse down behavior // is to select (or deselect) rows when the mouse is released. We don't // want the selection to change when the user checks or unchecks a checkbox, so if the // mouse down event was to check/uncheck, we have to hide this mouse // down event from the control. int x = m.LParam.ToInt32() & 0xFFFF; int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; return this.ProcessLButtonDown(this.OlvHitTest(x, y)); } /// /// Handle a left mouse down at the given hit test location /// /// Subclasses can override this to do something unique /// /// True if the message has been handled protected virtual bool ProcessLButtonDown(OlvListViewHitTestInfo hti) { if (hti.Item == null) return false; // If the click occurs on a button, ignore it so the row isn't selected if (hti.HitTestLocation == HitTestLocation.Button) { this.Invalidate(); return true; } // If they didn't click checkbox, we can just return if (hti.HitTestLocation != HitTestLocation.CheckBox) return false; // Disabled rows cannot change checkboxes if (!hti.Item.Enabled) return true; // Did they click a sub item checkbox? if (hti.Column != null && hti.Column.Index > 0) { if (hti.Column.IsEditable && hti.Item.Enabled) this.ToggleSubItemCheckBox(hti.RowObject, hti.Column); return true; } // They must have clicked the primary checkbox this.ToggleCheckObject(hti.RowObject); // If they change the checkbox of a selected row, all the rows in the selection // should be given the same state if (hti.Item.Selected) { CheckState? state = this.GetCheckState(hti.RowObject); if (state.HasValue) { foreach (Object x in this.SelectedObjects) this.SetObjectCheckedness(x, state.Value); } } return true; } /// /// Catch the Left Button up event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleLButtonUp(ref Message m) { if (this.MouseMoveHitTest == null) return false; int x = m.LParam.ToInt32() & 0xFFFF; int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; // Did they click an enabled, non-empty button? if (this.MouseMoveHitTest.HitTestLocation == HitTestLocation.Button) { // If a button was hit, Item and Column must be non-null if (this.MouseMoveHitTest.Item.Enabled || this.MouseMoveHitTest.Column.EnableButtonWhenItemIsDisabled) { string buttonText = this.MouseMoveHitTest.Column.GetStringValue(this.MouseMoveHitTest.RowObject); if (!String.IsNullOrEmpty(buttonText)) { this.Invalidate(); CellClickEventArgs args = new CellClickEventArgs(); this.BuildCellEvent(args, new Point(x, y), this.MouseMoveHitTest); this.OnButtonClick(args); return true; } } } // Are they trying to expand/collapse a group? if (this.MouseMoveHitTest.HitTestLocation == HitTestLocation.GroupExpander) { if (this.TriggerGroupExpandCollapse(this.MouseMoveHitTest.Group)) return true; } if (ObjectListView.IsVistaOrLater && this.HasCollapsibleGroups) base.DefWndProc(ref m); return false; } /// /// Trigger a GroupExpandCollapse event and return true if the action was cancelled /// /// /// protected virtual bool TriggerGroupExpandCollapse(OLVGroup group) { GroupExpandingCollapsingEventArgs args = new GroupExpandingCollapsingEventArgs(group); this.OnGroupExpandingCollapsing(args); return args.Canceled; } /// /// Catch the Right Button down event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleRButtonDown(ref Message m) { int x = m.LParam.ToInt32() & 0xFFFF; int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; return this.ProcessRButtonDown(this.OlvHitTest(x, y)); } /// /// Handle a left mouse down at the given hit test location /// /// Subclasses can override this to do something unique /// /// True if the message has been handled protected virtual bool ProcessRButtonDown(OlvListViewHitTestInfo hti) { if (hti.Item == null) return false; // Ignore clicks on checkboxes return (hti.HitTestLocation == HitTestLocation.CheckBox); } /// /// Catch the Left Button double click event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleLButtonDoubleClick(ref Message m) { int x = m.LParam.ToInt32() & 0xFFFF; int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; return this.ProcessLButtonDoubleClick(this.OlvHitTest(x, y)); } /// /// Handle a mouse double click at the given hit test location /// /// Subclasses can override this to do something unique /// /// True if the message has been handled protected virtual bool ProcessLButtonDoubleClick(OlvListViewHitTestInfo hti) { // If the user double clicked on a checkbox, ignore it return (hti.HitTestLocation == HitTestLocation.CheckBox); } /// /// Catch the right Button double click event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleRButtonDoubleClick(ref Message m) { int x = m.LParam.ToInt32() & 0xFFFF; int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; return this.ProcessRButtonDoubleClick(this.OlvHitTest(x, y)); } /// /// Handle a right mouse double click at the given hit test location /// /// Subclasses can override this to do something unique /// /// True if the message has been handled protected virtual bool ProcessRButtonDoubleClick(OlvListViewHitTestInfo hti) { // If the user double clicked on a checkbox, ignore it return (hti.HitTestLocation == HitTestLocation.CheckBox); } /// /// Catch the MouseMove event. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleMouseMove(ref Message m) { //int x = m.LParam.ToInt32() & 0xFFFF; //int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; //this.lastMouseMoveX = x; //this.lastMouseMoveY = y; return false; } //private int lastMouseMoveX = -1; //private int lastMouseMoveY = -1; /// /// Handle notifications that have been reflected back from the parent window /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleReflectNotify(ref Message m) { const int NM_CLICK = -2; const int NM_DBLCLK = -3; const int NM_RDBLCLK = -6; const int NM_CUSTOMDRAW = -12; const int NM_RELEASEDCAPTURE = -16; const int LVN_FIRST = -100; const int LVN_ITEMCHANGED = LVN_FIRST - 1; const int LVN_ITEMCHANGING = LVN_FIRST - 0; const int LVN_HOTTRACK = LVN_FIRST - 21; const int LVN_MARQUEEBEGIN = LVN_FIRST - 56; const int LVN_GETINFOTIP = LVN_FIRST - 58; const int LVN_GETDISPINFO = LVN_FIRST - 77; const int LVN_BEGINSCROLL = LVN_FIRST - 80; const int LVN_ENDSCROLL = LVN_FIRST - 81; const int LVN_LINKCLICK = LVN_FIRST - 84; const int LVN_GROUPINFO = LVN_FIRST - 88; // undocumented const int LVIF_STATE = 8; //const int LVIS_FOCUSED = 1; const int LVIS_SELECTED = 2; bool isMsgHandled = false; // TODO: Don't do any logic in this method. Create separate methods for each message NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); //System.Diagnostics.Debug.WriteLine(String.Format("rn: {0}", nmhdr->code)); switch (nmhdr.code) { case NM_CLICK: // The standard ListView does some strange stuff here when the list has checkboxes. // If you shift click on non-primary columns when FullRowSelect is true, the // checkedness of the selected rows changes. // We can't just not do the base class stuff because it sets up state that is used to // determine mouse up events later on. // So, we sabotage the base class's process of the click event. The base class does a HITTEST // in order to determine which row was clicked -- if that fails, the base class does nothing. // So when we get a CLICK, we know that the base class is going to send a HITTEST very soon, // so we ignore the next HITTEST message, which will cause the click processing to fail. //System.Diagnostics.Debug.WriteLine("NM_CLICK"); this.skipNextHitTest = true; break; case LVN_BEGINSCROLL: //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); isMsgHandled = this.HandleBeginScroll(ref m); break; case LVN_ENDSCROLL: isMsgHandled = this.HandleEndScroll(ref m); break; case LVN_LINKCLICK: isMsgHandled = this.HandleLinkClick(ref m); break; case LVN_MARQUEEBEGIN: //System.Diagnostics.Debug.WriteLine("LVN_MARQUEEBEGIN"); this.isMarqueSelecting = true; break; case LVN_GETINFOTIP: //System.Diagnostics.Debug.WriteLine("LVN_GETINFOTIP"); // When virtual lists are in SmallIcon view, they generates tooltip message with invalid item indicies. NativeMethods.NMLVGETINFOTIP nmGetInfoTip = (NativeMethods.NMLVGETINFOTIP)m.GetLParam(typeof(NativeMethods.NMLVGETINFOTIP)); isMsgHandled = nmGetInfoTip.iItem >= this.GetItemCount(); break; case NM_RELEASEDCAPTURE: //System.Diagnostics.Debug.WriteLine("NM_RELEASEDCAPTURE"); this.isMarqueSelecting = false; this.Invalidate(); break; case NM_CUSTOMDRAW: //System.Diagnostics.Debug.WriteLine("NM_CUSTOMDRAW"); isMsgHandled = this.HandleCustomDraw(ref m); break; case NM_DBLCLK: // The default behavior of a .NET ListView with checkboxes is to toggle the checkbox on // double-click. That's just silly, if you ask me :) if (this.CheckBoxes) { // How do we make ListView not do that silliness? We could just ignore the message // but the last part of the base code sets up state information, and without that // state, the ListView doesn't trigger MouseDoubleClick events. So we fake a // right button double click event, which sets up the same state, but without // toggling the checkbox. nmhdr.code = NM_RDBLCLK; Marshal.StructureToPtr(nmhdr, m.LParam, false); } break; case LVN_ITEMCHANGED: //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGED"); NativeMethods.NMLISTVIEW nmlistviewPtr2 = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); if ((nmlistviewPtr2.uChanged & LVIF_STATE) != 0) { CheckState currentValue = this.CalculateCheckState(nmlistviewPtr2.uOldState); CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr2.uNewState); if (currentValue != newCheckValue) { // Remove the state indicies so that we don't trigger the OnItemChecked method // when we call our base method after exiting this method nmlistviewPtr2.uOldState = (nmlistviewPtr2.uOldState & 0x0FFF); nmlistviewPtr2.uNewState = (nmlistviewPtr2.uNewState & 0x0FFF); Marshal.StructureToPtr(nmlistviewPtr2, m.LParam, false); } else { bool isSelected = (nmlistviewPtr2.uNewState & LVIS_SELECTED) == LVIS_SELECTED; if (isSelected) { // System.Diagnostics.Debug.WriteLine(String.Format("Selected: {0}", nmlistviewPtr2.iItem)); bool isShiftDown = (Control.ModifierKeys & Keys.Shift) == Keys.Shift; // -1 indicates that all rows are to be selected -- in fact, they already have been. // We now have to deselect all the disabled objects. if (nmlistviewPtr2.iItem == -1 || isShiftDown) { Stopwatch sw = Stopwatch.StartNew(); foreach (object disabledModel in this.DisabledObjects) { int modelIndex = this.IndexOf(disabledModel); if (modelIndex >= 0) NativeMethods.DeselectOneItem(this, modelIndex); } System.Diagnostics.Debug.WriteLine(String.Format("PERF - Deselecting took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks)); } else { // If the object just selected is disabled, explicitly de-select it OLVListItem olvi = this.GetItem(nmlistviewPtr2.iItem); if (olvi != null && !olvi.Enabled) NativeMethods.DeselectOneItem(this, nmlistviewPtr2.iItem); } } } } break; case LVN_ITEMCHANGING: //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGING"); NativeMethods.NMLISTVIEW nmlistviewPtr = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); if ((nmlistviewPtr.uChanged & LVIF_STATE) != 0) { CheckState currentValue = this.CalculateCheckState(nmlistviewPtr.uOldState); CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr.uNewState); if (currentValue != newCheckValue) { // Prevent the base method from seeing the state change, // since we handled it elsewhere nmlistviewPtr.uChanged &= ~LVIF_STATE; Marshal.StructureToPtr(nmlistviewPtr, m.LParam, false); } } break; case LVN_HOTTRACK: break; case LVN_GETDISPINFO: break; case LVN_GROUPINFO: //System.Diagnostics.Debug.WriteLine("reflect notify: GROUP INFO"); isMsgHandled = this.HandleGroupInfo(ref m); break; //default: //System.Diagnostics.Debug.WriteLine(String.Format("reflect notify: {0}", nmhdr.code)); //break; } return isMsgHandled; } private bool skipNextHitTest; private CheckState CalculateCheckState(int state) { switch ((state & 0xf000) >> 12) { case 1: return CheckState.Unchecked; case 2: return CheckState.Checked; case 3: return CheckState.Indeterminate; default: return CheckState.Checked; } } /// /// In the notification messages, we handle attempts to change the width of our columns /// /// The m to be processed /// bool to indicate if the msg has been handled protected bool HandleNotify(ref Message m) { bool isMsgHandled = false; const int NM_CUSTOMDRAW = -12; const int HDN_FIRST = (0 - 300); const int HDN_ITEMCHANGINGA = (HDN_FIRST - 0); const int HDN_ITEMCHANGINGW = (HDN_FIRST - 20); const int HDN_ITEMCLICKA = (HDN_FIRST - 2); const int HDN_ITEMCLICKW = (HDN_FIRST - 22); const int HDN_DIVIDERDBLCLICKA = (HDN_FIRST - 5); const int HDN_DIVIDERDBLCLICKW = (HDN_FIRST - 25); const int HDN_BEGINTRACKA = (HDN_FIRST - 6); const int HDN_BEGINTRACKW = (HDN_FIRST - 26); const int HDN_ENDTRACKA = (HDN_FIRST - 7); const int HDN_ENDTRACKW = (HDN_FIRST - 27); const int HDN_TRACKA = (HDN_FIRST - 8); const int HDN_TRACKW = (HDN_FIRST - 28); // Handle the notification, remembering to handle both ANSI and Unicode versions NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); //System.Diagnostics.Debug.WriteLine(String.Format("not: {0}", nmhdr->code)); //if (nmhdr.code < HDN_FIRST) // System.Diagnostics.Debug.WriteLine(nmhdr.code); // In KB Article #183258, MS states that when a header control has the HDS_FULLDRAG style, it will receive // ITEMCHANGING events rather than TRACK events. Under XP SP2 (at least) this is not always true, which may be // why MS has withdrawn that particular KB article. It is true that the header is always given the HDS_FULLDRAG // style. But even while window style set, the control doesn't always received ITEMCHANGING events. // The controlling setting seems to be the Explorer option "Show Window Contents While Dragging"! // In the category of "truly bizarre side effects", if the this option is turned on, we will receive // ITEMCHANGING events instead of TRACK events. But if it is turned off, we receive lots of TRACK events and // only one ITEMCHANGING event at the very end of the process. // If we receive HDN_TRACK messages, it's harder to control the resizing process. If we return a result of 1, we // cancel the whole drag operation, not just that particular track event, which is clearly not what we want. // If we are willing to compile with unsafe code enabled, we can modify the size of the column in place, using the // commented out code below. But without unsafe code, the best we can do is allow the user to drag the column to // any width, and then spring it back to within bounds once they release the mouse button. UI-wise it's very ugly. switch (nmheader.nhdr.code) { case NM_CUSTOMDRAW: if (!this.OwnerDrawnHeader) isMsgHandled = this.HeaderControl.HandleHeaderCustomDraw(ref m); break; case HDN_ITEMCLICKA: case HDN_ITEMCLICKW: if (!this.PossibleFinishCellEditing()) { m.Result = (IntPtr)1; // prevent the change from happening isMsgHandled = true; } break; case HDN_DIVIDERDBLCLICKA: case HDN_DIVIDERDBLCLICKW: case HDN_BEGINTRACKA: case HDN_BEGINTRACKW: if (!this.PossibleFinishCellEditing()) { m.Result = (IntPtr)1; // prevent the change from happening isMsgHandled = true; break; } if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { OLVColumn column = this.GetColumn(nmheader.iItem); // Space filling columns can't be dragged or double-click resized if (column.FillsFreeSpace) { m.Result = (IntPtr)1; // prevent the change from happening isMsgHandled = true; } } break; case HDN_ENDTRACKA: case HDN_ENDTRACKW: //if (this.ShowGroups) // this.ResizeLastGroup(); break; case HDN_TRACKA: case HDN_TRACKW: if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); OLVColumn column = this.GetColumn(nmheader.iItem); if (hditem.cxy < column.MinimumWidth) hditem.cxy = column.MinimumWidth; else if (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth) hditem.cxy = column.MaximumWidth; Marshal.StructureToPtr(hditem, nmheader.pHDITEM, false); } break; case HDN_ITEMCHANGINGA: case HDN_ITEMCHANGINGW: nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); OLVColumn column = this.GetColumn(nmheader.iItem); // Check the mask to see if the width field is valid, and if it is, make sure it's within range if ((hditem.mask & 1) == 1) { if (hditem.cxy < column.MinimumWidth || (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth)) { m.Result = (IntPtr)1; // prevent the change from happening isMsgHandled = true; } } } break; case ToolTipControl.TTN_SHOW: //System.Diagnostics.Debug.WriteLine("olv TTN_SHOW"); if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) isMsgHandled = this.CellToolTip.HandleShow(ref m); break; case ToolTipControl.TTN_POP: //System.Diagnostics.Debug.WriteLine("olv TTN_POP"); if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) isMsgHandled = this.CellToolTip.HandlePop(ref m); break; case ToolTipControl.TTN_GETDISPINFO: //System.Diagnostics.Debug.WriteLine("olv TTN_GETDISPINFO"); if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) isMsgHandled = this.CellToolTip.HandleGetDispInfo(ref m); break; // default: // System.Diagnostics.Debug.WriteLine(String.Format("notify: {0}", nmheader.nhdr.code)); // break; } return isMsgHandled; } /// /// Create a ToolTipControl to manage the tooltip control used by the listview control /// protected virtual void CreateCellToolTip() { this.cellToolTip = new ToolTipControl(); this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); this.cellToolTip.Showing += new EventHandler(HandleCellToolTipShowing); this.cellToolTip.SetMaxWidth(); NativeMethods.MakeTopMost(this.cellToolTip); } /// /// Update the handle used by our cell tooltip to be the tooltip used by /// the underlying Windows listview control. /// protected virtual void UpdateCellToolTipHandle() { if (this.cellToolTip != null && this.cellToolTip.Handle == IntPtr.Zero) this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); } /// /// Handle the WM_PAINT event /// /// /// Return true if the msg has been handled and nothing further should be done protected virtual bool HandlePaint(ref Message m) { //System.Diagnostics.Debug.WriteLine("> WMPAINT"); // We only want to custom draw the control within WmPaint message and only // once per paint event. We use these bools to insure this. this.isInWmPaintEvent = true; this.shouldDoCustomDrawing = true; this.prePaintLevel = 0; this.ShowOverlays(); this.HandlePrePaint(); base.WndProc(ref m); this.HandlePostPaint(); this.isInWmPaintEvent = false; //System.Diagnostics.Debug.WriteLine("< WMPAINT"); return true; } private int prePaintLevel; /// /// Perform any steps needed before painting the control /// protected virtual void HandlePrePaint() { // When we get a WM_PAINT msg, remember the rectangle that is being updated. // We can't get this information later, since the BeginPaint call wipes it out. // this.lastUpdateRectangle = NativeMethods.GetUpdateRect(this); // we no longer need this, but keep the code so we can see it later //// When the list is empty, we want to handle the drawing of the control by ourselves. //// Unfortunately, there is no easy way to tell our superclass that we want to do this. //// So we resort to guile and deception. We validate the list area of the control, which //// effectively tells our superclass that this area does not need to be painted. //// Our superclass will then not paint the control, leaving us free to do so ourselves. //// Without doing this trickery, the superclass will draw the list as empty, //// and then moments later, we will draw the empty list msg, giving a nasty flicker //if (this.GetItemCount() == 0 && this.HasEmptyListMsg) // NativeMethods.ValidateRect(this, this.ClientRectangle); } /// /// Perform any steps needed after painting the control /// protected virtual void HandlePostPaint() { // This message is no longer necessary, but we keep it for compatibility } /// /// Handle the window position changing. /// /// The m to be processed /// bool to indicate if the msg has been handled protected virtual bool HandleWindowPosChanging(ref Message m) { const int SWP_NOSIZE = 1; NativeMethods.WINDOWPOS pos = (NativeMethods.WINDOWPOS)m.GetLParam(typeof(NativeMethods.WINDOWPOS)); if ((pos.flags & SWP_NOSIZE) == 0) { if (pos.cx < this.Bounds.Width) // only when shrinking // pos.cx is the window width, not the client area width, so we have to subtract the border widths this.ResizeFreeSpaceFillingColumns(pos.cx - (this.Bounds.Width - this.ClientSize.Width)); } return false; } #endregion #region Column header clicking, column hiding and resizing /// /// The user has right clicked on the column headers. Do whatever is required /// /// Return true if this event has been handle protected virtual bool HandleHeaderRightClick(int columnIndex) { ColumnClickEventArgs eventArgs = new ColumnClickEventArgs(columnIndex); this.OnColumnRightClick(eventArgs); // TODO: Allow users to say they have handled this event return this.ShowHeaderRightClickMenu(columnIndex, Cursor.Position); } /// /// Show a menu that is appropriate when the given column header is clicked. /// /// The index of the header that was clicked. This /// can be -1, indicating that the header was clicked outside of a column /// Where should the menu be shown /// True if a menu was displayed protected virtual bool ShowHeaderRightClickMenu(int columnIndex, Point pt) { ToolStripDropDown m = this.MakeHeaderRightClickMenu(columnIndex); if (m.Items.Count > 0) { m.Show(pt); return true; } return false; } /// /// Create the menu that should be displayed when the user right clicks /// on the given column header. /// /// Index of the column that was right clicked. /// This can be negative, which indicates a click outside of any header. /// The toolstrip that should be displayed protected virtual ToolStripDropDown MakeHeaderRightClickMenu(int columnIndex) { ToolStripDropDown m = new ContextMenuStrip(); if (columnIndex >= 0 && this.UseFiltering && this.ShowFilterMenuOnRightClick) m = this.MakeFilteringMenu(m, columnIndex); if (columnIndex >= 0 && this.ShowCommandMenuOnRightClick) m = this.MakeColumnCommandMenu(m, columnIndex); if (this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None) { m = this.MakeColumnSelectMenu(m); } return m; } /// /// The user has right clicked on the column headers. Do whatever is required /// /// Return true if this event has been handle [Obsolete("Use HandleHeaderRightClick(int) instead")] protected virtual bool HandleHeaderRightClick() { return false; } /// /// Show a popup menu at the given point which will allow the user to choose which columns /// are visible on this listview /// /// Where should the menu be placed [Obsolete("Use ShowHeaderRightClickMenu instead")] protected virtual void ShowColumnSelectMenu(Point pt) { ToolStripDropDown m = this.MakeColumnSelectMenu(new ContextMenuStrip()); m.Show(pt); } /// /// Show a popup menu at the given point which will allow the user to choose which columns /// are visible on this listview /// /// /// Where should the menu be placed [Obsolete("Use ShowHeaderRightClickMenu instead")] protected virtual void ShowColumnCommandMenu(int columnIndex, Point pt) { ToolStripDropDown m = this.MakeColumnCommandMenu(new ContextMenuStrip(), columnIndex); if (this.SelectColumnsOnRightClick) { if (m.Items.Count > 0) m.Items.Add(new ToolStripSeparator()); this.MakeColumnSelectMenu(m); } m.Show(pt); } /// /// Gets or set the text to be used for the sorting ascending command /// [Category("标签 - ObjectListView"), DefaultValue("按【{0}】增序"), Localizable(true)] public string MenuLabelSortAscending { get { return this.menuLabelSortAscending; } set { this.menuLabelSortAscending = value; } } private string menuLabelSortAscending = "按【{0}】增序"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("按【{0}】降序"), Localizable(true)] public string MenuLabelSortDescending { get { return this.menuLabelSortDescending; } set { this.menuLabelSortDescending = value; } } private string menuLabelSortDescending = "按【{0}】降序"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("按【{0}】分组"), Localizable(true)] public string MenuLabelGroupBy { get { return this.menuLabelGroupBy; } set { this.menuLabelGroupBy = value; } } private string menuLabelGroupBy = "按【{0}】分组"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("锁定按【{0}】分组"), Localizable(true)] public string MenuLabelLockGroupingOn { get { return this.menuLabelLockGroupingOn; } set { this.menuLabelLockGroupingOn = value; } } private string menuLabelLockGroupingOn = "锁定按【{0}】分组"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("不锁定按【{0}】分组"), Localizable(true)] public string MenuLabelUnlockGroupingOn { get { return this.menuLabelUnlockGroupingOn; } set { this.menuLabelUnlockGroupingOn = value; } } private string menuLabelUnlockGroupingOn = "不锁定按【{0}】分组"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("关闭分组"), Localizable(true)] public string MenuLabelTurnOffGroups { get { return this.menuLabelTurnOffGroups; } set { this.menuLabelTurnOffGroups = value; } } private string menuLabelTurnOffGroups = "关闭分组"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("不排序"), Localizable(true)] public string MenuLabelUnsort { get { return this.menuLabelUnsort; } set { this.menuLabelUnsort = value; } } private string menuLabelUnsort = "不排序"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("列"), Localizable(true)] public string MenuLabelColumns { get { return this.menuLabelColumns; } set { this.menuLabelColumns = value; } } private string menuLabelColumns = "列"; /// /// /// [Category("标签 - ObjectListView"), DefaultValue("选择列..."), Localizable(true)] public string MenuLabelSelectColumns { get { return this.menuLabelSelectColumns; } set { this.menuLabelSelectColumns = value; } } private string menuLabelSelectColumns = "选择列..."; /// /// Gets or sets the image that will be place next to the Sort Ascending command /// static public Bitmap SortAscendingImage = ryControls.Properties.Resources.SortAscending; /// /// Gets or sets the image that will be placed next to the Sort Descending command /// static public Bitmap SortDescendingImage = ryControls.Properties.Resources.SortDescending; /// /// Append the column selection menu items to the given menu strip. /// /// The menu to which the items will be added. /// /// Return the menu to which the items were added public virtual ToolStripDropDown MakeColumnCommandMenu(ToolStripDropDown strip, int columnIndex) { OLVColumn column = this.GetColumn(columnIndex); if (column == null) return strip; if (strip.Items.Count > 0) strip.Items.Add(new ToolStripSeparator()); string label = String.Format(this.MenuLabelSortAscending, column.Text); if (column.Sortable && !String.IsNullOrEmpty(label)) { strip.Items.Add(label, ObjectListView.SortAscendingImage, (EventHandler)delegate(object sender, EventArgs args) { this.Sort(column, SortOrder.Ascending); }); } label = String.Format(this.MenuLabelSortDescending, column.Text); if (column.Sortable && !String.IsNullOrEmpty(label)) { strip.Items.Add(label, ObjectListView.SortDescendingImage, (EventHandler)delegate(object sender, EventArgs args) { this.Sort(column, SortOrder.Descending); }); } if (this.CanShowGroups) { label = String.Format(this.MenuLabelGroupBy, column.Text); if (column.Groupable && !String.IsNullOrEmpty(label)) { strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { this.ShowGroups = true; this.PrimarySortColumn = column; this.PrimarySortOrder = SortOrder.Ascending; this.BuildList(); }); } } if (this.ShowGroups) { if (this.AlwaysGroupByColumn == column) { label = String.Format(this.MenuLabelUnlockGroupingOn, column.Text); if (!String.IsNullOrEmpty(label)) { strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { this.AlwaysGroupByColumn = null; this.AlwaysGroupBySortOrder = SortOrder.None; this.BuildList(); }); } } else { label = String.Format(this.MenuLabelLockGroupingOn, column.Text); if (column.Groupable && !String.IsNullOrEmpty(label)) { strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { this.ShowGroups = true; this.AlwaysGroupByColumn = column; this.AlwaysGroupBySortOrder = SortOrder.Ascending; this.BuildList(); }); } } label = String.Format(this.MenuLabelTurnOffGroups, column.Text); if (!String.IsNullOrEmpty(label)) { strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { this.ShowGroups = false; this.BuildList(); }); } } else { label = String.Format(this.MenuLabelUnsort, column.Text); if (column.Sortable && !String.IsNullOrEmpty(label) && this.PrimarySortOrder != SortOrder.None) { strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { this.Unsort(); }); } } return strip; } /// /// Append the column selection menu items to the given menu strip. /// /// The menu to which the items will be added. /// Return the menu to which the items were added public virtual ToolStripDropDown MakeColumnSelectMenu(ToolStripDropDown strip) { System.Diagnostics.Debug.Assert(this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None); // Append a separator if the menu isn't empty and the last item isn't already a separator if (strip.Items.Count > 0 && (!(strip.Items[strip.Items.Count-1] is ToolStripSeparator))) strip.Items.Add(new ToolStripSeparator()); if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) this.RememberDisplayIndicies(); if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.ModelDialog) { strip.Items.Add(this.MenuLabelSelectColumns, null, delegate(object sender, EventArgs args) { (new ColumnSelectionForm()).OpenOn(this); }); } if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.Submenu) { ToolStripMenuItem menu = new ToolStripMenuItem(this.MenuLabelColumns); menu.DropDownItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); strip.Items.Add(menu); this.AddItemsToColumnSelectMenu(menu.DropDownItems); } if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.InlineMenu) { strip.ItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); strip.Closing += new ToolStripDropDownClosingEventHandler(this.ColumnSelectMenuClosing); this.AddItemsToColumnSelectMenu(strip.Items); } return strip; } /// /// Create the menu items that will allow columns to be choosen and add them to the /// given collection /// /// protected void AddItemsToColumnSelectMenu(ToolStripItemCollection items) { // Sort columns by display order List columns = new List(this.AllColumns); columns.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); // Build menu from sorted columns foreach (OLVColumn col in columns) { ToolStripMenuItem mi = new ToolStripMenuItem(col.Text); mi.Checked = col.IsVisible; mi.Tag = col; // The 'Index' property returns -1 when the column is not visible, so if the // column isn't visible we have to enable the item. Also the first column can't be turned off mi.Enabled = !col.IsVisible || col.CanBeHidden; items.Add(mi); } } private void ColumnSelectMenuItemClicked(object sender, ToolStripItemClickedEventArgs e) { this.contextMenuStaysOpen = false; ToolStripMenuItem menuItemClicked = e.ClickedItem as ToolStripMenuItem; if (menuItemClicked == null) return; OLVColumn col = menuItemClicked.Tag as OLVColumn; if (col == null) return; menuItemClicked.Checked = !menuItemClicked.Checked; col.IsVisible = menuItemClicked.Checked; this.contextMenuStaysOpen = this.SelectColumnsMenuStaysOpen; this.BeginInvoke(new MethodInvoker(this.RebuildColumns)); } private bool contextMenuStaysOpen; private void ColumnSelectMenuClosing(object sender, ToolStripDropDownClosingEventArgs e) { e.Cancel = this.contextMenuStaysOpen && e.CloseReason == ToolStripDropDownCloseReason.ItemClicked; this.contextMenuStaysOpen = false; } /// /// Create a Filtering menu /// /// /// /// public virtual ToolStripDropDown MakeFilteringMenu(ToolStripDropDown strip, int columnIndex) { OLVColumn column = this.GetColumn(columnIndex); if (column == null) return strip; FilterMenuBuilder strategy = this.FilterMenuBuildStrategy; if (strategy == null) return strip; return strategy.MakeFilterMenu(strip, this, column); } /// /// Override the OnColumnReordered method to do what we want /// /// protected override void OnColumnReordered(ColumnReorderedEventArgs e) { base.OnColumnReordered(e); // The internal logic of the .NET code behind a ENDDRAG event means that, // at this point, the DisplayIndex's of the columns are not yet as they are // going to be. So we have to invoke a method to run later that will remember // what the real DisplayIndex's are. this.BeginInvoke(new MethodInvoker(this.RememberDisplayIndicies)); } private void RememberDisplayIndicies() { // Remember the display indexes so we can put them back at a later date foreach (OLVColumn x in this.AllColumns) x.LastDisplayIndex = x.DisplayIndex; } /// /// When the column widths are changing, resize the space filling columns /// /// /// protected virtual void HandleColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e) { if (this.UpdateSpaceFillingColumnsWhenDraggingColumnDivider && !this.GetColumn(e.ColumnIndex).FillsFreeSpace) { // If the width of a column is increasing, resize any space filling columns allowing the extra // space that the new column width is going to consume int oldWidth = this.GetColumn(e.ColumnIndex).Width; if (e.NewWidth > oldWidth) this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width - (e.NewWidth - oldWidth)); else this.ResizeFreeSpaceFillingColumns(); } } /// /// When the column widths change, resize the space filling columns /// /// /// protected virtual void HandleColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { if (!this.GetColumn(e.ColumnIndex).FillsFreeSpace) this.ResizeFreeSpaceFillingColumns(); } /// /// When the size of the control changes, we have to resize our space filling columns. /// /// /// protected virtual void HandleLayout(object sender, LayoutEventArgs e) { // We have to delay executing the recalculation of the columns, since virtual lists // get terribly confused if we resize the column widths during this event. if (!this.hasResizeColumnsHandler) { this.hasResizeColumnsHandler = true; this.RunWhenIdle(this.HandleApplicationIdleResizeColumns); } } private void RunWhenIdle(EventHandler eventHandler) { Application.Idle += eventHandler; if (!this.CanUseApplicationIdle) { SynchronizationContext.Current.Post(delegate(object x) { Application.RaiseIdle(EventArgs.Empty); }, null); } } /// /// Resize our space filling columns so they fill any unoccupied width in the control /// protected virtual void ResizeFreeSpaceFillingColumns() { this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width); } /// /// Resize our space filling columns so they fill any unoccupied width in the control /// protected virtual void ResizeFreeSpaceFillingColumns(int freeSpace) { // It's too confusing to dynamically resize columns at design time. if (this.DesignMode) return; if (this.Frozen) return; // Calculate the free space available int totalProportion = 0; List spaceFillingColumns = new List(); for (int i = 0; i < this.Columns.Count; i++) { OLVColumn col = this.GetColumn(i); if (col.FillsFreeSpace) { spaceFillingColumns.Add(col); totalProportion += col.FreeSpaceProportion; } else freeSpace -= col.Width; } freeSpace = Math.Max(0, freeSpace); // Any space filling column that would hit it's Minimum or Maximum // width must be treated as a fixed column. foreach (OLVColumn col in spaceFillingColumns.ToArray()) { int newWidth = (freeSpace * col.FreeSpaceProportion) / totalProportion; if (col.MinimumWidth != -1 && newWidth < col.MinimumWidth) newWidth = col.MinimumWidth; else if (col.MaximumWidth != -1 && newWidth > col.MaximumWidth) newWidth = col.MaximumWidth; else newWidth = 0; if (newWidth > 0) { col.Width = newWidth; freeSpace -= newWidth; totalProportion -= col.FreeSpaceProportion; spaceFillingColumns.Remove(col); } } // Distribute the free space between the columns foreach (OLVColumn col in spaceFillingColumns) { col.Width = (freeSpace*col.FreeSpaceProportion)/totalProportion; } } #endregion #region Checkboxes /// /// Check all rows /// public virtual void CheckAll() { this.CheckedObjects = EnumerableToArray(this.Objects, false); } /// /// Check the checkbox in the given column header /// /// If the given columns header check box is linked to the cell check boxes, /// then checkboxes in all cells will also be checked. /// public virtual void CheckHeaderCheckBox(OLVColumn column) { if (column == null) return; ChangeHeaderCheckBoxState(column, CheckState.Checked); } /// /// Mark the checkbox in the given column header as having an indeterminate value /// /// public virtual void CheckIndeterminateHeaderCheckBox(OLVColumn column) { if (column == null) return; ChangeHeaderCheckBoxState(column, CheckState.Indeterminate); } /// /// Mark the given object as indeterminate check state /// /// The model object to be marked indeterminate public virtual void CheckIndeterminateObject(object modelObject) { this.SetObjectCheckedness(modelObject, CheckState.Indeterminate); } /// /// Mark the given object as checked in the list /// /// The model object to be checked public virtual void CheckObject(object modelObject) { this.SetObjectCheckedness(modelObject, CheckState.Checked); } /// /// Mark the given objects as checked in the list /// /// The model object to be checked public virtual void CheckObjects(IEnumerable modelObjects) { foreach (object model in modelObjects) this.CheckObject(model); } /// /// Put a check into the check box at the given cell /// /// /// public virtual void CheckSubItem(object rowObject, OLVColumn column) { if (column == null || rowObject == null || !column.CheckBoxes) return; column.PutCheckState(rowObject, CheckState.Checked); this.RefreshObject(rowObject); } /// /// Put an indeterminate check into the check box at the given cell /// /// /// public virtual void CheckIndeterminateSubItem(object rowObject, OLVColumn column) { if (column == null || rowObject == null || !column.CheckBoxes) return; column.PutCheckState(rowObject, CheckState.Indeterminate); this.RefreshObject(rowObject); } /// /// Return true of the given object is checked /// /// The model object whose checkedness is returned /// Is the given object checked? /// If the given object is not in the list, this method returns false. public virtual bool IsChecked(object modelObject) { return this.GetCheckState(modelObject) == CheckState.Checked; } /// /// Return true of the given object is indeterminately checked /// /// The model object whose checkedness is returned /// Is the given object indeterminately checked? /// If the given object is not in the list, this method returns false. public virtual bool IsCheckedIndeterminate(object modelObject) { return this.GetCheckState(modelObject) == CheckState.Indeterminate; } /// /// Is there a check at the check box at the given cell /// /// /// public virtual bool IsSubItemChecked(object rowObject, OLVColumn column) { if (column == null || rowObject == null || !column.CheckBoxes) return false; return (column.GetCheckState(rowObject) == CheckState.Checked); } /// /// Get the checkedness of an object from the model. Returning null means the /// model does not know and the value from the control will be used. /// /// /// protected virtual CheckState? GetCheckState(Object modelObject) { if (this.CheckStateGetter != null) return this.CheckStateGetter(modelObject); return this.PersistentCheckBoxes ? this.GetPersistentCheckState(modelObject) : (CheckState?)null; } /// /// Record the change of checkstate for the given object in the model. /// This does not update the UI -- only the model /// /// /// /// The check state that was recorded and that should be used to update /// the control. protected virtual CheckState PutCheckState(Object modelObject, CheckState state) { if (this.CheckStatePutter != null) return this.CheckStatePutter(modelObject, state); return this.PersistentCheckBoxes ? this.SetPersistentCheckState(modelObject, state) : state; } /// /// Change the check state of the given object to be the given state. /// /// /// If the given model object isn't in the list, we still try to remember /// its state, in case it is referenced in the future. /// /// /// True if the checkedness of the model changed protected virtual bool SetObjectCheckedness(object modelObject, CheckState state) { if (GetCheckState(modelObject) == state) return false; OLVListItem olvi = this.ModelToItem(modelObject); // If we didn't find the given, we still try to record the check state. if (olvi == null) { this.PutCheckState(modelObject, state); return true; } // Trigger checkbox changing event ItemCheckEventArgs ice = new ItemCheckEventArgs(olvi.Index, state, olvi.CheckState); this.OnItemCheck(ice); if (ice.NewValue == olvi.CheckState) return false; olvi.CheckState = this.PutCheckState(modelObject, state); this.RefreshItem(olvi); // Trigger check changed event this.OnItemChecked(new ItemCheckedEventArgs(olvi)); return true; } /// /// Toggle the checkedness of the given object. A checked object becomes /// unchecked; an unchecked or indeterminate object becomes checked. /// If the list has tristate checkboxes, the order is: /// unchecked -> checked -> indeterminate -> unchecked ... /// /// The model object to be checked public virtual void ToggleCheckObject(object modelObject) { OLVListItem olvi = this.ModelToItem(modelObject); if (olvi == null) return; CheckState newState = CheckState.Checked; if (olvi.CheckState == CheckState.Checked) { newState = this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; } else { if (olvi.CheckState == CheckState.Indeterminate && this.TriStateCheckBoxes) newState = CheckState.Unchecked; } this.SetObjectCheckedness(modelObject, newState); } /// /// Toggle the checkbox in the header of the given column /// /// Obviously, this is only useful if the column actually has a header checkbox. /// public virtual void ToggleHeaderCheckBox(OLVColumn column) { if (column == null) return; CheckState newState = CalculateToggledCheckState(column.HeaderCheckState, column.HeaderTriStateCheckBox, column.HeaderCheckBoxDisabled); ChangeHeaderCheckBoxState(column, newState); } private void ChangeHeaderCheckBoxState(OLVColumn column, CheckState newState) { // Tell the world the checkbox was clicked HeaderCheckBoxChangingEventArgs args = new HeaderCheckBoxChangingEventArgs(); args.Column = column; args.NewCheckState = newState; this.OnHeaderCheckBoxChanging(args); if (args.Cancel || column.HeaderCheckState == args.NewCheckState) return; Stopwatch sw = Stopwatch.StartNew(); column.HeaderCheckState = args.NewCheckState; this.HeaderControl.Invalidate(column); if (column.HeaderCheckBoxUpdatesRowCheckBoxes) { if (column.Index == 0) this.UpdateAllPrimaryCheckBoxes(column); else this.UpdateAllSubItemCheckBoxes(column); } Debug.WriteLine(String.Format("PERF - Changing row checkboxes on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); } private void UpdateAllPrimaryCheckBoxes(OLVColumn column) { if (!this.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) return; if (column.HeaderCheckState == CheckState.Checked) CheckAll(); else UncheckAll(); } private void UpdateAllSubItemCheckBoxes(OLVColumn column) { if (!column.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) return; foreach (object model in this.Objects) column.PutCheckState(model, column.HeaderCheckState); this.BuildList(true); } /// /// Toggle the check at the check box of the given cell /// /// /// public virtual void ToggleSubItemCheckBox(object rowObject, OLVColumn column) { CheckState currentState = column.GetCheckState(rowObject); CheckState newState = CalculateToggledCheckState(currentState, column.TriStateCheckBoxes, false); SubItemCheckingEventArgs args = new SubItemCheckingEventArgs(column, this.ModelToItem(rowObject), column.Index, currentState, newState); this.OnSubItemChecking(args); if (args.Canceled) return; switch (args.NewValue) { case CheckState.Checked: this.CheckSubItem(rowObject, column); break; case CheckState.Indeterminate: this.CheckIndeterminateSubItem(rowObject, column); break; case CheckState.Unchecked: this.UncheckSubItem(rowObject, column); break; } } /// /// Uncheck all rows /// public virtual void UncheckAll() { this.CheckedObjects = null; } /// /// Mark the given object as unchecked in the list /// /// The model object to be unchecked public virtual void UncheckObject(object modelObject) { this.SetObjectCheckedness(modelObject, CheckState.Unchecked); } /// /// Mark the given objects as unchecked in the list /// /// The model object to be checked public virtual void UncheckObjects(IEnumerable modelObjects) { foreach (object model in modelObjects) this.UncheckObject(model); } /// /// Uncheck the checkbox in the given column header /// /// public virtual void UncheckHeaderCheckBox(OLVColumn column) { if (column == null) return; ChangeHeaderCheckBoxState(column, CheckState.Unchecked); } /// /// Uncheck the check at the given cell /// /// /// public virtual void UncheckSubItem(object rowObject, OLVColumn column) { if (column == null || rowObject == null || !column.CheckBoxes) return; column.PutCheckState(rowObject, CheckState.Unchecked); this.RefreshObject(rowObject); } #endregion #region OLV accessing /// /// Return the column at the given index /// /// Index of the column to be returned /// An OLVColumn public virtual OLVColumn GetColumn(int index) { return (OLVColumn)this.Columns[index]; } /// /// Return the column at the given title. /// /// Name of the column to be returned /// An OLVColumn public virtual OLVColumn GetColumn(string name) { foreach (ColumnHeader column in this.Columns) { if (column.Text == name) return (OLVColumn)column; } return null; } /// /// Return a collection of columns that are visible to the given view. /// Only Tile and Details have columns; all other views have 0 columns. /// /// Which view are the columns being calculate for? /// A list of columns public virtual List GetFilteredColumns(View view) { // For both detail and tile view, the first column must be included. Normally, we would // use the ColumnHeader.Index property, but if the header is not currently part of a ListView // that property returns -1. So, we track the index of // the column header, and always include the first header. int index = 0; return this.AllColumns.FindAll(delegate(OLVColumn x) { return (index++ == 0) || x.IsVisible; }); } /// /// Return the number of items in the list /// /// the number of items in the list /// If a filter is installed, this will return the number of items that match the filter. public virtual int GetItemCount() { return this.Items.Count; } /// /// Return the item at the given index /// /// Index of the item to be returned /// An OLVListItem public virtual OLVListItem GetItem(int index) { if (index < 0 || index >= this.GetItemCount()) return null; return (OLVListItem)this.Items[index]; } /// /// Return the model object at the given index /// /// Index of the model object to be returned /// A model object public virtual object GetModelObject(int index) { OLVListItem item = this.GetItem(index); return item == null ? null : item.RowObject; } /// /// Find the item and column that are under the given co-ords /// /// X co-ord /// Y co-ord /// The column under the given point /// The item under the given point. Can be null. public virtual OLVListItem GetItemAt(int x, int y, out OLVColumn hitColumn) { hitColumn = null; ListViewHitTestInfo info = this.HitTest(x, y); if (info.Item == null) return null; if (info.SubItem != null) { int subItemIndex = info.Item.SubItems.IndexOf(info.SubItem); hitColumn = this.GetColumn(subItemIndex); } return (OLVListItem)info.Item; } /// /// Return the sub item at the given index/column /// /// Index of the item to be returned /// Index of the subitem to be returned /// An OLVListSubItem public virtual OLVListSubItem GetSubItem(int index, int columnIndex) { OLVListItem olvi = this.GetItem(index); return olvi == null ? null : olvi.GetSubItem(columnIndex); } #endregion #region Object manipulation /// /// Scroll the listview so that the given group is at the top. /// /// The group to be revealed /// /// If the group is already visible, the list will still be scrolled to move /// the group to the top, if that is possible. /// /// This only works when the list is showing groups (obviously). /// This does not work on virtual lists, since virtual lists don't use ListViewGroups /// for grouping. Use instead. /// public virtual void EnsureGroupVisible(ListViewGroup lvg) { if (!this.ShowGroups || lvg == null) return; int groupIndex = this.Groups.IndexOf(lvg); if (groupIndex <= 0) { // There is no easy way to scroll back to the beginning of the list int delta = 0 - NativeMethods.GetScrollPosition(this, false); NativeMethods.Scroll(this, 0, delta); } else { // Find the display rectangle of the last item in the previous group ListViewGroup previousGroup = this.Groups[groupIndex - 1]; ListViewItem lastItemInGroup = previousGroup.Items[previousGroup.Items.Count - 1]; Rectangle r = this.GetItemRect(lastItemInGroup.Index); // Scroll so that the last item of the previous group is just out of sight, // which will make the desired group header visible. int delta = r.Y + r.Height / 2; NativeMethods.Scroll(this, 0, delta); } } /// /// Ensure that the given model object is visible /// /// The model object to be revealed public virtual void EnsureModelVisible(Object modelObject) { int index = this.IndexOf(modelObject); if (index >= 0) this.EnsureVisible(index); } /// /// Return the model object of the row that is selected or null if there is no selection or more than one selection /// /// Model object or null [Obsolete("Use SelectedObject property instead of this method")] public virtual object GetSelectedObject() { return this.SelectedObject; } /// /// Return the model objects of the rows that are selected or an empty collection if there is no selection /// /// ArrayList [Obsolete("Use SelectedObjects property instead of this method")] public virtual ArrayList GetSelectedObjects() { return ObjectListView.EnumerableToArray(this.SelectedObjects, false); } /// /// Return the model object of the row that is checked or null if no row is checked /// or more than one row is checked /// /// Model object or null /// Use CheckedObject property instead of this method [Obsolete("Use CheckedObject property instead of this method")] public virtual object GetCheckedObject() { return this.CheckedObject; } /// /// Get the collection of model objects that are checked. /// /// Use CheckedObjects property instead of this method [Obsolete("Use CheckedObjects property instead of this method")] public virtual ArrayList GetCheckedObjects() { return ObjectListView.EnumerableToArray(this.CheckedObjects, false); } /// /// Find the given model object within the listview and return its index /// /// The model object to be found /// The index of the object. -1 means the object was not present public virtual int IndexOf(Object modelObject) { for (int i = 0; i < this.GetItemCount(); i++) { if (this.GetModelObject(i).Equals(modelObject)) return i; } return -1; } /// /// Rebuild the given ListViewItem with the data from its associated model. /// /// This method does not resort or regroup the view. It simply updates /// the displayed data of the given item public virtual void RefreshItem(OLVListItem olvi) { olvi.UseItemStyleForSubItems = true; olvi.SubItems.Clear(); this.FillInValues(olvi, olvi.RowObject); this.PostProcessOneRow(olvi.Index, this.GetDisplayOrderOfItemIndex(olvi.Index), olvi); } /// /// Rebuild the data on the row that is showing the given object. /// /// /// /// This method does not resort or regroup the view. /// /// /// The given object is *not* used as the source of data for the rebuild. /// It is only used to locate the matching model in the collection, /// then that matching model is used as the data source. This distinction is /// only important in model classes that have overridden the Equals() method. /// /// /// If you want the given model object to replace the pre-existing model, /// use . /// /// public virtual void RefreshObject(object modelObject) { this.RefreshObjects(new object[] { modelObject }); } /// /// Update the rows that are showing the given objects /// /// /// This method does not resort or regroup the view. /// This method can safely be called from background threads. /// public virtual void RefreshObjects(IList modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); return; } foreach (object modelObject in modelObjects) { OLVListItem olvi = this.ModelToItem(modelObject); if (olvi != null) { this.ReplaceModel(olvi, modelObject); this.RefreshItem(olvi); } } } private void ReplaceModel(OLVListItem olvi, object newModel) { if (ReferenceEquals(olvi.RowObject, newModel)) return; this.TakeOwnershipOfObjects(); ArrayList array = ObjectListView.EnumerableToArray(this.Objects, false); int i = array.IndexOf(olvi.RowObject); if (i >= 0) array[i] = newModel; olvi.RowObject = newModel; } /// /// Update the rows that are selected /// /// This method does not resort or regroup the view. public virtual void RefreshSelectedObjects() { foreach (ListViewItem lvi in this.SelectedItems) this.RefreshItem((OLVListItem)lvi); } /// /// Select the row that is displaying the given model object, in addition to any current selection. /// /// The object to be selected /// Use the property to deselect all other rows public virtual void SelectObject(object modelObject) { this.SelectObject(modelObject, false); } /// /// Select the row that is displaying the given model object, in addition to any current selection. /// /// The object to be selected /// Should the object be focused as well? /// Use the property to deselect all other rows public virtual void SelectObject(object modelObject, bool setFocus) { OLVListItem olvi = this.ModelToItem(modelObject); if (olvi != null && olvi.Enabled) { olvi.Selected = true; if (setFocus) olvi.Focused = true; } } /// /// Select the rows that is displaying any of the given model object. All other rows are deselected. /// /// A collection of model objects public virtual void SelectObjects(IList modelObjects) { this.SelectedIndices.Clear(); if (modelObjects == null) return; foreach (object modelObject in modelObjects) { OLVListItem olvi = this.ModelToItem(modelObject); if (olvi != null && olvi.Enabled) olvi.Selected = true; } } #endregion #region Freezing/Suspending /// /// Get or set whether or not the listview is frozen. When the listview is /// frozen, it will not update itself. /// /// The Frozen property is similar to the methods Freeze()/Unfreeze() /// except that setting Frozen property to false immediately unfreezes the control /// regardless of the number of Freeze() calls outstanding. /// objectListView1.Frozen = false; // unfreeze the control now! /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual bool Frozen { get { return freezeCount > 0; } set { if (value) Freeze(); else if (freezeCount > 0) { freezeCount = 1; Unfreeze(); } } } private int freezeCount; /// /// Freeze the listview so that it no longer updates itself. /// /// Freeze()/Unfreeze() calls nest correctly public virtual void Freeze() { if (freezeCount == 0) DoFreeze(); freezeCount++; this.OnFreezing(new FreezeEventArgs(freezeCount)); } /// /// Unfreeze the listview. If this call is the outermost Unfreeze(), /// the contents of the listview will be rebuilt. /// /// Freeze()/Unfreeze() calls nest correctly public virtual void Unfreeze() { if (freezeCount <= 0) return; freezeCount--; if (freezeCount == 0) DoUnfreeze(); this.OnFreezing(new FreezeEventArgs(freezeCount)); } /// /// Do the actual work required when the listview is frozen /// protected virtual void DoFreeze() { this.BeginUpdate(); } /// /// Do the actual work required when the listview is unfrozen /// protected virtual void DoUnfreeze() { this.EndUpdate(); this.ResizeFreeSpaceFillingColumns(); this.BuildList(); } /// /// Returns true if selection events are currently suspended. /// While selection events are suspended, neither SelectedIndexChanged /// or SelectionChanged events will be raised. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] protected bool SelectionEventsSuspended { get { return this.suspendSelectionEventCount > 0; } } /// /// Suspend selection events until a matching ResumeSelectionEvents() /// is called. /// /// Calls to this method nest correctly. Every call to SuspendSelectionEvents() /// must have a matching ResumeSelectionEvents(). protected void SuspendSelectionEvents() { this.suspendSelectionEventCount++; } /// /// Resume raising selection events. /// protected void ResumeSelectionEvents() { Debug.Assert(this.SelectionEventsSuspended, "Mismatched called to ResumeSelectionEvents()"); this.suspendSelectionEventCount--; } /// /// Returns a disposable that will disable selection events /// during a using() block. /// /// protected IDisposable SuspendSelectionEventsDuring() { return new SuspendSelectionDisposable(this); } /// /// Implementation only class that suspends and resumes selection /// events on instance creation and disposal. /// private class SuspendSelectionDisposable : IDisposable { public SuspendSelectionDisposable(ObjectListView objectListView) { this.objectListView = objectListView; this.objectListView.SuspendSelectionEvents(); } public void Dispose() { this.objectListView.ResumeSelectionEvents(); } private readonly ObjectListView objectListView; } #endregion #region Column sorting /// /// Sort the items by the last sort column and order /// new public void Sort() { this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); } /// /// Sort the items in the list view by the values in the given column and the last sort order /// /// The name of the column whose values will be used for the sorting public virtual void Sort(string columnToSortName) { this.Sort(this.GetColumn(columnToSortName), this.PrimarySortOrder); } /// /// Sort the items in the list view by the values in the given column and the last sort order /// /// The index of the column whose values will be used for the sorting public virtual void Sort(int columnToSortIndex) { if (columnToSortIndex >= 0 && columnToSortIndex < this.Columns.Count) this.Sort(this.GetColumn(columnToSortIndex), this.PrimarySortOrder); } /// /// Sort the items in the list view by the values in the given column and the last sort order /// /// The column whose values will be used for the sorting public virtual void Sort(OLVColumn columnToSort) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort); }); } else { this.Sort(columnToSort, this.PrimarySortOrder); } } /// /// Sort the items in the list view by the values in the given column and by the given order. /// /// The column whose values will be used for the sorting. /// If null, the first column will be used. /// The ordering to be used for sorting. If this is None, /// this.Sorting and then SortOrder.Ascending will be used /// If ShowGroups is true, the rows will be grouped by the given column. /// If AlwaysGroupsByColumn is not null, the rows will be grouped by that column, /// and the rows within each group will be sorted by the given column. public virtual void Sort(OLVColumn columnToSort, SortOrder order) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort, order); }); } else { this.DoSort(columnToSort, order); this.PostProcessRows(); } } private void DoSort(OLVColumn columnToSort, SortOrder order) { // Sanity checks if (this.GetItemCount() == 0 || this.Columns.Count == 0) return; // Fill in default values, if the parameters don't make sense if (this.ShowGroups) { columnToSort = columnToSort ?? this.GetColumn(0); if (order == SortOrder.None) { order = this.Sorting; if (order == SortOrder.None) order = SortOrder.Ascending; } } // Give the world a chance to fiddle with or completely avoid the sorting process BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(columnToSort, order); this.OnBeforeSorting(args); if (args.Canceled) return; // Virtual lists don't preserve selection, so we have to do it specifically // THINK: Do we need to preserve focus too? IList selection = this.VirtualMode ? this.SelectedObjects : null; this.SuspendSelectionEvents(); this.ClearHotItem(); // Finally, do the work of sorting, unless an event handler has already done the sorting for us if (!args.Handled) { // Sanity checks if (args.ColumnToSort != null && args.SortOrder != SortOrder.None) { if (this.ShowGroups) this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, args.ColumnToSort, args.SortOrder, args.SecondaryColumnToSort, args.SecondarySortOrder); else if (this.CustomSorter != null) this.CustomSorter(args.ColumnToSort, args.SortOrder); else this.ListViewItemSorter = new ColumnComparer(args.ColumnToSort, args.SortOrder, args.SecondaryColumnToSort, args.SecondarySortOrder); } } if (this.ShowSortIndicators) this.ShowSortIndicator(args.ColumnToSort, args.SortOrder); this.PrimarySortColumn = args.ColumnToSort; this.PrimarySortOrder = args.SortOrder; if (selection != null && selection.Count > 0) this.SelectedObjects = selection; this.ResumeSelectionEvents(); this.RefreshHotItem(); this.OnAfterSorting(new AfterSortingEventArgs(args)); } /// /// Put a sort indicator next to the text of the sort column /// public virtual void ShowSortIndicator() { if (this.ShowSortIndicators && this.PrimarySortOrder != SortOrder.None) this.ShowSortIndicator(this.PrimarySortColumn, this.PrimarySortOrder); } /// /// Put a sort indicator next to the text of the given given column /// /// The column to be marked /// The sort order in effect on that column protected virtual void ShowSortIndicator(OLVColumn columnToSort, SortOrder sortOrder) { int imageIndex = -1; if (!NativeMethods.HasBuiltinSortIndicators()) { // If we can't use builtin image, we have to make and then locate the index of the // sort indicator we want to use. SortOrder.None doesn't show an image. if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(SORT_INDICATOR_UP_KEY)) MakeSortIndicatorImages(); if (this.SmallImageList != null) { string key = sortOrder == SortOrder.Ascending ? SORT_INDICATOR_UP_KEY : SORT_INDICATOR_DOWN_KEY; imageIndex = this.SmallImageList.Images.IndexOfKey(key); } } // Set the image for each column for (int i = 0; i < this.Columns.Count; i++) { if (columnToSort != null && i == columnToSort.Index) NativeMethods.SetColumnImage(this, i, sortOrder, imageIndex); else NativeMethods.SetColumnImage(this, i, SortOrder.None, -1); } } /// /// The name of the image used when a column is sorted ascending /// /// This image is only used on pre-XP systems. System images are used for XP and later public const string SORT_INDICATOR_UP_KEY = "sort-indicator-up"; /// /// The name of the image used when a column is sorted descending /// /// This image is only used on pre-XP systems. System images are used for XP and later public const string SORT_INDICATOR_DOWN_KEY = "sort-indicator-down"; /// /// If the sort indicator images don't already exist, this method will make and install them /// protected virtual void MakeSortIndicatorImages() { // Don't mess with the image list in design mode if (this.DesignMode) return; ImageList il = this.SmallImageList; if (il == null) { il = new ImageList(); il.ImageSize = new Size(16, 16); il.ColorDepth = ColorDepth.Depth32Bit; } // This arrangement of points works well with (16,16) images, and OK with others int midX = il.ImageSize.Width / 2; int midY = (il.ImageSize.Height / 2) - 1; int deltaX = midX - 2; int deltaY = deltaX / 2; if (il.Images.IndexOfKey(SORT_INDICATOR_UP_KEY) == -1) { Point pt1 = new Point(midX - deltaX, midY + deltaY); Point pt2 = new Point(midX, midY - deltaY - 1); Point pt3 = new Point(midX + deltaX, midY + deltaY); il.Images.Add(SORT_INDICATOR_UP_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); } if (il.Images.IndexOfKey(SORT_INDICATOR_DOWN_KEY) == -1) { Point pt1 = new Point(midX - deltaX, midY - deltaY); Point pt2 = new Point(midX, midY + deltaY); Point pt3 = new Point(midX + deltaX, midY - deltaY); il.Images.Add(SORT_INDICATOR_DOWN_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); } this.SmallImageList = il; } private Bitmap MakeTriangleBitmap(Size sz, Point[] pts) { Bitmap bm = new Bitmap(sz.Width, sz.Height); Graphics g = Graphics.FromImage(bm); g.FillPolygon(new SolidBrush(Color.Gray), pts); return bm; } /// /// Remove any sorting and revert to the given order of the model objects /// public virtual void Unsort() { this.ShowGroups = false; this.PrimarySortColumn = null; this.PrimarySortOrder = SortOrder.None; this.BuildList(); } #endregion #region Utilities private static CheckState CalculateToggledCheckState(CheckState currentState, bool isTriState, bool isDisabled) { if (isDisabled) return currentState; switch (currentState) { case CheckState.Checked: return isTriState ? CheckState.Indeterminate : CheckState.Unchecked; case CheckState.Indeterminate: return CheckState.Unchecked; default: return CheckState.Checked; } } /// /// Do the actual work of creating the given list of groups /// /// protected virtual void CreateGroups(IEnumerable groups) { this.Groups.Clear(); // The group must be added before it is given items, otherwise an exception is thrown (is this documented?) foreach (OLVGroup group in groups) { group.InsertGroupOldStyle(this); group.SetItemsOldStyle(); } } /// /// For some reason, UseItemStyleForSubItems doesn't work for the colors /// when owner drawing the list, so we have to specifically give each subitem /// the desired colors /// /// The item whose subitems are to be corrected /// Cells drawn via BaseRenderer don't need this, but it is needed /// when an owner drawn cell uses DrawDefault=true protected virtual void CorrectSubItemColors(ListViewItem olvi) { } /// /// Fill in the given OLVListItem with values of the given row /// /// the OLVListItem that is to be stuff with values /// the model object from which values will be taken protected virtual void FillInValues(OLVListItem lvi, object rowObject) { if (this.Columns.Count == 0) return; OLVListSubItem subItem = this.MakeSubItem(rowObject, this.GetColumn(0)); lvi.SubItems[0] = subItem; lvi.ImageSelector = subItem.ImageSelector; // Give the item the same font/colors as the control lvi.Font = this.Font; lvi.BackColor = this.BackColor; lvi.ForeColor = this.ForeColor; // Should the row be selectable? lvi.Enabled = !this.IsDisabled(rowObject); // Only Details and Tile views have subitems switch (this.View) { case View.Details: for (int i = 1; i < this.Columns.Count; i++) { lvi.SubItems.Add(this.MakeSubItem(rowObject, this.GetColumn(i))); } break; case View.Tile: for (int i = 1; i < this.Columns.Count; i++) { OLVColumn column = this.GetColumn(i); if (column.IsTileViewColumn) lvi.SubItems.Add(this.MakeSubItem(rowObject, column)); } break; } // Should the row be selectable? if (!lvi.Enabled) { lvi.UseItemStyleForSubItems = false; ApplyRowStyle(lvi, this.DisabledItemStyle ?? ObjectListView.DefaultDisabledItemStyle); } // Set the check state of the row, if we are showing check boxes if (this.CheckBoxes) { CheckState? state = this.GetCheckState(lvi.RowObject); if (state.HasValue) lvi.CheckState = state.Value; } // Give the RowFormatter a chance to mess with the item if (this.RowFormatter != null) { this.RowFormatter(lvi); } } private OLVListSubItem MakeSubItem(object rowObject, OLVColumn column) { object cellValue = column.GetValue(rowObject); OLVListSubItem subItem = new OLVListSubItem(cellValue, column.ValueToString(cellValue), column.GetImage(rowObject)); if (this.UseHyperlinks && column.Hyperlink) { IsHyperlinkEventArgs args = new IsHyperlinkEventArgs(); args.ListView = this; args.Model = rowObject; args.Column = column; args.Text = subItem.Text; args.Url = subItem.Text; args.IsHyperlink = !this.IsDisabled(rowObject); this.OnIsHyperlink(args); subItem.Url = args.IsHyperlink ? args.Url : null; } return subItem; } private void ApplyHyperlinkStyle(OLVListItem olvi) { for (int i = 0; i < this.Columns.Count; i++) { OLVListSubItem subItem = olvi.GetSubItem(i); if (subItem == null) continue; OLVColumn column = this.GetColumn(i); if (column.Hyperlink && !String.IsNullOrEmpty(subItem.Url)) this.ApplyCellStyle(olvi, i, this.IsUrlVisited(subItem.Url) ? this.HyperlinkStyle.Visited : this.HyperlinkStyle.Normal); } } /// /// Make sure the ListView has the extended style that says to display subitem images. /// /// This method must be called after any .NET call that update the extended styles /// since they seem to erase this setting. protected virtual void ForceSubItemImagesExStyle() { // Virtual lists can't show subitem images natively, so don't turn on this flag if (!this.VirtualMode) NativeMethods.ForceSubItemImagesExStyle(this); } /// /// Convert the given image selector to an index into our image list. /// Return -1 if that's not possible /// /// /// Index of the image in the imageList, or -1 protected virtual int GetActualImageIndex(Object imageSelector) { if (imageSelector == null) return -1; if (imageSelector is Int32) return (int)imageSelector; String imageSelectorAsString = imageSelector as String; if (imageSelectorAsString != null && this.SmallImageList != null) return this.SmallImageList.Images.IndexOfKey(imageSelectorAsString); return -1; } /// /// Return the tooltip that should be shown when the mouse is hovered over the given column /// /// The column index whose tool tip is to be fetched /// A string or null if no tool tip is to be shown public virtual String GetHeaderToolTip(int columnIndex) { OLVColumn column = this.GetColumn(columnIndex); if (column == null) return null; String tooltip = column.ToolTipText; if (this.HeaderToolTipGetter != null) tooltip = this.HeaderToolTipGetter(column); return tooltip; } /// /// Return the tooltip that should be shown when the mouse is hovered over the given cell /// /// The column index whose tool tip is to be fetched /// The row index whose tool tip is to be fetched /// A string or null if no tool tip is to be shown public virtual String GetCellToolTip(int columnIndex, int rowIndex) { if (this.CellToolTipGetter != null) return this.CellToolTipGetter(this.GetColumn(columnIndex), this.GetModelObject(rowIndex)); // Show the URL in the tooltip if it's different to the text if (columnIndex >= 0) { OLVListSubItem subItem = this.GetSubItem(rowIndex, columnIndex); if (subItem != null && !String.IsNullOrEmpty(subItem.Url) && subItem.Url != subItem.Text && this.HotCellHitLocation == HitTestLocation.Text) return subItem.Url; } return null; } /// /// Return the OLVListItem that displays the given model object /// /// The modelObject whose item is to be found /// The OLVListItem that displays the model, or null /// This method has O(n) performance. public virtual OLVListItem ModelToItem(object modelObject) { if (modelObject == null) return null; foreach (OLVListItem olvi in this.Items) { if (olvi.RowObject != null && olvi.RowObject.Equals(modelObject)) return olvi; } return null; } /// /// Do the work required after the items in a listview have been created /// protected virtual void PostProcessRows() { // If this method is called during a BeginUpdate/EndUpdate pair, changes to the // Items collection are cached. Getting the Count flushes that cache. #pragma warning disable 168 // ReSharper disable once UnusedVariable int count = this.Items.Count; #pragma warning restore 168 int i = 0; if (this.ShowGroups) { foreach (ListViewGroup group in this.Groups) { foreach (OLVListItem olvi in group.Items) { this.PostProcessOneRow(olvi.Index, i, olvi); i++; } } } else { foreach (OLVListItem olvi in this.Items) { this.PostProcessOneRow(olvi.Index, i, olvi); i++; } } } /// /// Do the work required after one item in a listview have been created /// protected virtual void PostProcessOneRow(int rowIndex, int displayIndex, OLVListItem olvi) { if (this.UseAlternatingBackColors && this.View == View.Details && olvi.Enabled) { olvi.UseItemStyleForSubItems = true; olvi.BackColor = displayIndex % 2 == 1 ? this.AlternateRowBackColorOrDefault : this.BackColor; } if (this.ShowImagesOnSubItems && !this.VirtualMode) this.SetSubItemImages(rowIndex, olvi); bool needToTriggerFormatCellEvents = this.TriggerFormatRowEvent(rowIndex, displayIndex, olvi); // We only need cell level events if we are in details view if (this.View != View.Details) return; // If we're going to have per cell formatting, we need to copy the formatting // of the item into each cell, before triggering the cell format events if (needToTriggerFormatCellEvents) { PropagateFormatFromRowToCells(olvi); this.TriggerFormatCellEvents(rowIndex, displayIndex, olvi); } // Similarly, if any cell in the row has hyperlinks, we have to copy formatting // from the item into each cell before applying the hyperlink style if (this.UseHyperlinks && olvi.HasAnyHyperlinks) { PropagateFormatFromRowToCells(olvi); this.ApplyHyperlinkStyle(olvi); } } /// /// Prepare the listview to show alternate row backcolors /// /// We cannot rely on lvi.Index in this method. /// In a straight list, lvi.Index is the display index, and can be used to determine /// whether the row should be colored. But when organised by groups, lvi.Index is not /// useable because it still refers to the position in the overall list, not the display order. /// [Obsolete("This method is no longer used. Override PostProcessOneRow() to achieve a similar result")] protected virtual void PrepareAlternateBackColors() { } /// /// Setup all subitem images on all rows /// [Obsolete("This method is not longer maintained and will be removed", false)] protected virtual void SetAllSubItemImages() { //if (!this.ShowImagesOnSubItems || this.OwnerDraw) // return; //this.ForceSubItemImagesExStyle(); //for (int rowIndex = 0; rowIndex < this.GetItemCount(); rowIndex++) // SetSubItemImages(rowIndex, this.GetItem(rowIndex)); } /// /// Tell the underlying list control which images to show against the subitems /// /// the index at which the item occurs /// the item whose subitems are to be set protected virtual void SetSubItemImages(int rowIndex, OLVListItem item) { this.SetSubItemImages(rowIndex, item, false); } /// /// Tell the underlying list control which images to show against the subitems /// /// the index at which the item occurs /// the item whose subitems are to be set /// will existing images be cleared if no new image is provided? protected virtual void SetSubItemImages(int rowIndex, OLVListItem item, bool shouldClearImages) { if (!this.ShowImagesOnSubItems || this.OwnerDraw) return; for (int i = 1; i < item.SubItems.Count; i++) { this.SetSubItemImage(rowIndex, i, item.GetSubItem(i), shouldClearImages); } } /// /// Set the subitem image natively /// /// /// /// /// public virtual void SetSubItemImage(int rowIndex, int subItemIndex, OLVListSubItem subItem, bool shouldClearImages) { int imageIndex = this.GetActualImageIndex(subItem.ImageSelector); if (shouldClearImages || imageIndex != -1) NativeMethods.SetSubItemImage(this, rowIndex, subItemIndex, imageIndex); } /// /// Take ownership of the 'objects' collection. This separats our collection from the source. /// /// /// /// This method /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject /// calls will modify our collection and not the original colleciton. /// /// /// This method has the intentional side-effect of converting our list of objects to an ArrayList. /// /// protected virtual void TakeOwnershipOfObjects() { if (this.isOwnerOfObjects) return; this.isOwnerOfObjects = true; this.objects = ObjectListView.EnumerableToArray(this.objects, true); } /// /// Trigger FormatRow and possibly FormatCell events for the given item /// /// /// /// protected virtual bool TriggerFormatRowEvent(int rowIndex, int displayIndex, OLVListItem olvi) { FormatRowEventArgs args = new FormatRowEventArgs(); args.ListView = this; args.RowIndex = rowIndex; args.DisplayIndex = displayIndex; args.Item = olvi; args.UseCellFormatEvents = this.UseCellFormatEvents; this.OnFormatRow(args); return args.UseCellFormatEvents; } /// /// Trigger FormatCell events for the given item /// /// /// /// protected virtual void TriggerFormatCellEvents(int rowIndex, int displayIndex, OLVListItem olvi) { PropagateFormatFromRowToCells(olvi); // Fire one event per cell FormatCellEventArgs args2 = new FormatCellEventArgs(); args2.ListView = this; args2.RowIndex = rowIndex; args2.DisplayIndex = displayIndex; args2.Item = olvi; for (int i = 0; i < this.Columns.Count; i++) { args2.ColumnIndex = i; args2.Column = this.GetColumn(i); args2.SubItem = olvi.GetSubItem(i); this.OnFormatCell(args2); } } private static void PropagateFormatFromRowToCells(OLVListItem olvi) { // If a cell isn't given its own colors, it *should* use the colors of the item. // However, there is a bug in the .NET framework where the cell are given // the colors of the ListView instead of the colors of the row. // If we've already done this, don't do it again if (olvi.UseItemStyleForSubItems == false) return; // So we have to explicitly give each cell the fore and back colors and the font that it should have. olvi.UseItemStyleForSubItems = false; Color backColor = olvi.BackColor; Color foreColor = olvi.ForeColor; Font font = olvi.Font; foreach (OLVListSubItem subitem in olvi.SubItems) { subitem.BackColor = backColor; subitem.ForeColor = foreColor; subitem.Font = font; } } /// /// Make the list forget everything -- all rows and all columns /// /// Use if you want to remove just the rows. public virtual void Reset() { this.Clear(); this.AllColumns.Clear(); this.ClearObjects(); this.PrimarySortColumn = null; this.SecondarySortColumn = null; this.ClearDisabledObjects(); this.ClearPersistentCheckState(); this.ClearUrlVisited(); this.ClearHotItem(); } #endregion #region ISupportInitialize Members void ISupportInitialize.BeginInit() { this.Frozen = true; } void ISupportInitialize.EndInit() { if (this.RowHeight != -1) { this.SmallImageList = this.SmallImageList; if (this.CheckBoxes) this.InitializeStateImageList(); } if (this.UseSubItemCheckBoxes || (this.VirtualMode && this.CheckBoxes)) this.SetupSubItemCheckBoxes(); this.Frozen = false; } #endregion #region Image list manipulation /// /// Update our externally visible image list so it holds the same images as our shadow list, but sized correctly /// private void SetupBaseImageList() { // If a row height hasn't been set, or an image list has been give which is the required size, just assign it if (rowHeight == -1 || this.View != View.Details || (this.shadowedImageList != null && this.shadowedImageList.ImageSize.Height == rowHeight)) this.BaseSmallImageList = this.shadowedImageList; else { int width = (this.shadowedImageList == null ? 16 : this.shadowedImageList.ImageSize.Width); this.BaseSmallImageList = this.MakeResizedImageList(width, rowHeight, shadowedImageList); } } /// /// Return a copy of the given source image list, where each image has been resized to be height x height in size. /// If source is null, an empty image list of the given size is returned /// /// Height and width of the new images /// Height and width of the new images /// Source of the images (can be null) /// A new image list private ImageList MakeResizedImageList(int width, int height, ImageList source) { ImageList il = new ImageList(); il.ImageSize = new Size(width, height); // If there's nothing to copy, just return the new list if (source == null) return il; il.TransparentColor = source.TransparentColor; il.ColorDepth = source.ColorDepth; // Fill the imagelist with resized copies from the source for (int i = 0; i < source.Images.Count; i++) { Bitmap bm = this.MakeResizedImage(width, height, source.Images[i], source.TransparentColor); il.Images.Add(bm); } // Give each image the same key it has in the original foreach (String key in source.Images.Keys) { il.Images.SetKeyName(source.Images.IndexOfKey(key), key); } return il; } /// /// Return a bitmap of the given height x height, which shows the given image, centred. /// /// Height and width of new bitmap /// Height and width of new bitmap /// Image to be centred /// The background color /// A new bitmap private Bitmap MakeResizedImage(int width, int height, Image image, Color transparent) { Bitmap bm = new Bitmap(width, height); Graphics g = Graphics.FromImage(bm); g.Clear(transparent); int x = Math.Max(0, (bm.Size.Width - image.Size.Width) / 2); int y = Math.Max(0, (bm.Size.Height - image.Size.Height) / 2); g.DrawImage(image, x, y, image.Size.Width, image.Size.Height); return bm; } /// /// Initialize the state image list with the required checkbox images /// protected virtual void InitializeStateImageList() { if (this.DesignMode) return; if (!this.CheckBoxes) return; if (this.StateImageList == null) { this.StateImageList = new ImageList(); this.StateImageList.ImageSize = new Size(16, this.RowHeight == -1 ? 16 : this.RowHeight); this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; } if (this.RowHeight != -1 && this.View == View.Details && this.StateImageList.ImageSize.Height != this.RowHeight) { this.StateImageList = new ImageList(); this.StateImageList.ImageSize = new Size(16, this.RowHeight); this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; } // The internal logic of ListView cycles through the state images when the primary // checkbox is clicked. So we have to get exactly the right number of images in the // image list. if (this.StateImageList.Images.Count == 0) this.AddCheckStateBitmap(this.StateImageList, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); if (this.StateImageList.Images.Count <= 1) this.AddCheckStateBitmap(this.StateImageList, CHECKED_KEY, CheckBoxState.CheckedNormal); if (this.TriStateCheckBoxes && this.StateImageList.Images.Count <= 2) this.AddCheckStateBitmap(this.StateImageList, INDETERMINATE_KEY, CheckBoxState.MixedNormal); else { if (this.StateImageList.Images.ContainsKey(INDETERMINATE_KEY)) this.StateImageList.Images.RemoveByKey(INDETERMINATE_KEY); } } /// /// The name of the image used when a check box is checked /// public const string CHECKED_KEY = "checkbox-checked"; /// /// The name of the image used when a check box is unchecked /// public const string UNCHECKED_KEY = "checkbox-unchecked"; /// /// The name of the image used when a check box is Indeterminate /// public const string INDETERMINATE_KEY = "checkbox-indeterminate"; /// /// Setup this control so it can display check boxes on subitems /// (or primary checkboxes in virtual mode) /// /// This gives the ListView a small image list, if it doesn't already have one. public virtual void SetupSubItemCheckBoxes() { this.ShowImagesOnSubItems = true; if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(CHECKED_KEY)) this.InitializeSubItemCheckBoxImages(); } /// /// Make sure the small image list for this control has checkbox images /// (used for sub-item checkboxes). /// /// /// /// This gives the ListView a small image list, if it doesn't already have one. /// /// /// ObjectListView has to manage checkboxes on subitems separate from the checkboxes on each row. /// The underlying ListView knows about the per-row checkboxes, and to make them work, OLV has to /// correctly configure the StateImageList. However, the ListView cannot do checkboxes in subitems, /// so ObjectListView has to handle them in a differnt fashion. So, per-row checkboxes are controlled /// by images in the StateImageList, but per-cell checkboxes are handled by images in the SmallImageList. /// /// protected virtual void InitializeSubItemCheckBoxImages() { // Don't mess with the image list in design mode if (this.DesignMode) return; ImageList il = this.SmallImageList; if (il == null) { il = new ImageList(); il.ImageSize = new Size(16, 16); il.ColorDepth = ColorDepth.Depth32Bit; } this.AddCheckStateBitmap(il, CHECKED_KEY, CheckBoxState.CheckedNormal); this.AddCheckStateBitmap(il, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); this.AddCheckStateBitmap(il, INDETERMINATE_KEY, CheckBoxState.MixedNormal); this.SmallImageList = il; } private void AddCheckStateBitmap(ImageList il, string key, CheckBoxState boxState) { Bitmap b = new Bitmap(il.ImageSize.Width, il.ImageSize.Height); Graphics g = Graphics.FromImage(b); g.Clear(il.TransparentColor); Point location = new Point(b.Width / 2 - 5, b.Height / 2 - 6); CheckBoxRenderer.DrawCheckBox(g, location, boxState); il.Images.Add(key, b); } #endregion #region Owner drawing /// /// Owner draw the column header /// /// protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e) { e.DrawDefault = true; base.OnDrawColumnHeader(e); } /// /// Owner draw the item /// /// protected override void OnDrawItem(DrawListViewItemEventArgs e) { if (this.View == View.Details) e.DrawDefault = false; else { if (this.ItemRenderer == null) e.DrawDefault = true; else { Object row = ((OLVListItem)e.Item).RowObject; e.DrawDefault = !this.ItemRenderer.RenderItem(e, e.Graphics, e.Bounds, row); } } if (e.DrawDefault) base.OnDrawItem(e); } /// /// Owner draw a single subitem /// /// protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e) { //System.Diagnostics.Debug.WriteLine(String.Format("OnDrawSubItem ({0}, {1})", e.ItemIndex, e.ColumnIndex)); // Don't try to do owner drawing at design time if (this.DesignMode) { e.DrawDefault = true; return; } object rowObject = ((OLVListItem)e.Item).RowObject; // Calculate where the subitem should be drawn Rectangle r = e.Bounds; // Get the special renderer for this column. If there isn't one, use the default draw mechanism. OLVColumn column = this.GetColumn(e.ColumnIndex); IRenderer renderer = this.GetCellRenderer(rowObject, column); // Get a graphics context for the renderer to use. // But we have more complications. Virtual lists have a nasty habit of drawing column 0 // whenever there is any mouse move events over a row, and doing it in an un-double-buffered manner, // which results in nasty flickers! There are also some unbuffered draw when a mouse is first // hovered over column 0 of a normal row. So, to avoid all complications, // we always manually double-buffer the drawing. // Except with Mono, which doesn't seem to handle double buffering at all :-( BufferedGraphics buffer = BufferedGraphicsManager.Current.Allocate(e.Graphics, r); Graphics g = buffer.Graphics; g.TextRenderingHint = ObjectListView.TextRenderingHint; g.SmoothingMode = ObjectListView.SmoothingMode; // Finally, give the renderer a chance to draw something e.DrawDefault = !renderer.RenderSubItem(e, g, r, rowObject); if (!e.DrawDefault) buffer.Render(); buffer.Dispose(); } #endregion #region OnEvent Handling /// /// We need the click count in the mouse up event, but that is always 1. /// So we have to remember the click count from the preceding mouse down event. /// /// protected override void OnMouseDown(MouseEventArgs e) { this.lastMouseDownClickCount = e.Clicks; base.OnMouseDown(e); } private int lastMouseDownClickCount; /// /// When the mouse leaves the control, remove any hot item highlighting /// /// protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); if (!this.Created) return; this.UpdateHotItem(new Point(-1,-1)); } // We could change the hot item on the mouse hover event, but it looks wrong. //protected override void OnMouseHover(EventArgs e) { // System.Diagnostics.Debug.WriteLine(String.Format("OnMouseHover")); // base.OnMouseHover(e); // this.UpdateHotItem(this.PointToClient(Cursor.Position)); //} /// /// When the mouse moves, we might need to change the hot item. /// /// protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (this.Created) HandleMouseMove(e.Location); } internal void HandleMouseMove(Point pt) { //System.Diagnostics.Debug.WriteLine(String.Format("HandleMouseMove: {0}", pt)); CellOverEventArgs args = new CellOverEventArgs(); this.BuildCellEvent(args, pt); this.OnCellOver(args); this.MouseMoveHitTest = args.HitTest; if (!args.Handled) this.UpdateHotItem(args.HitTest); } /// /// Check to see if we need to start editing a cell /// /// protected override void OnMouseUp(MouseEventArgs e) { //System.Diagnostics.Debug.WriteLine(String.Format("OnMouseUp")); base.OnMouseUp(e); if (!this.Created) return; if (e.Button == MouseButtons.Right) { this.OnRightMouseUp(e); return; } // What event should we listen for to start cell editing? // ------------------------------------------------------ // // We can't use OnMouseClick, OnMouseDoubleClick, OnClick, or OnDoubleClick // since they are not triggered for clicks on subitems without Full Row Select. // // We could use OnMouseDown, but selecting rows is done in OnMouseUp. This means // that if we start the editing during OnMouseDown, the editor will automatically // lose focus when mouse up happens. // // Tell the world about a cell click. If someone handles it, don't do anything else CellClickEventArgs args = new CellClickEventArgs(); this.BuildCellEvent(args, e.Location); args.ClickCount = this.lastMouseDownClickCount; this.OnCellClick(args); if (args.Handled) return; // Did the user click a hyperlink? if (this.UseHyperlinks && args.HitTest.HitTestLocation == HitTestLocation.Text && args.SubItem != null && !String.IsNullOrEmpty(args.SubItem.Url)) { // We have to delay the running of this process otherwise we can generate // a series of MouseUp events (don't ask me why) this.BeginInvoke((MethodInvoker)delegate { this.ProcessHyperlinkClicked(args); }); } // No one handled it so check to see if we should start editing. if (!this.ShouldStartCellEdit(e)) return; // We only start the edit if the user clicked on the image or text. if (args.HitTest.HitTestLocation == HitTestLocation.Nothing) return; // We don't edit the primary column by single clicks -- only subitems. if (this.CellEditActivation == CellEditActivateMode.SingleClick && args.ColumnIndex <= 0) return; // Don't start a cell edit operation when the user clicks on the background of a checkbox column -- it just looks wrong. // If the user clicks on the actual checkbox, changing the checkbox state is handled elsewhere. if (args.Column != null && args.Column.CheckBoxes) return; this.EditSubItem(args.Item, args.ColumnIndex); } /// /// Tell the world that a hyperlink was clicked and if the event isn't handled, /// do the default processing. /// /// protected virtual void ProcessHyperlinkClicked(CellClickEventArgs e) { HyperlinkClickedEventArgs args = new HyperlinkClickedEventArgs(); args.HitTest = e.HitTest; args.ListView = this; args.Location = new Point(-1, -1); args.Item = e.Item; args.SubItem = e.SubItem; args.Model = e.Model; args.ColumnIndex = e.ColumnIndex; args.Column = e.Column; args.RowIndex = e.RowIndex; args.ModifierKeys = Control.ModifierKeys; args.Url = e.SubItem.Url; this.OnHyperlinkClicked(args); if (!args.Handled) { this.StandardHyperlinkClickedProcessing(args); } } /// /// Do the default processing for a hyperlink clicked event, which /// is to try and open the url. /// /// protected virtual void StandardHyperlinkClickedProcessing(HyperlinkClickedEventArgs args) { Cursor originalCursor = this.Cursor; try { this.Cursor = Cursors.WaitCursor; System.Diagnostics.Process.Start(args.Url); } catch (Win32Exception) { System.Media.SystemSounds.Beep.Play(); // ignore it } finally { this.Cursor = originalCursor; } this.MarkUrlVisited(args.Url); this.RefreshHotItem(); } /// /// The user right clicked on the control /// /// protected virtual void OnRightMouseUp(MouseEventArgs e) { CellRightClickEventArgs args = new CellRightClickEventArgs(); this.BuildCellEvent(args, e.Location); this.OnCellRightClick(args); if (!args.Handled) { if (args.MenuStrip != null) { args.MenuStrip.Show(this, args.Location); } } } internal void BuildCellEvent(CellEventArgs args, Point location) { BuildCellEvent(args, location, this.OlvHitTest(location.X, location.Y)); } internal void BuildCellEvent(CellEventArgs args, Point location, OlvListViewHitTestInfo hitTest) { args.HitTest = hitTest; args.ListView = this; args.Location = location; args.Item = hitTest.Item; args.SubItem = hitTest.SubItem; args.Model = hitTest.RowObject; args.ColumnIndex = hitTest.ColumnIndex; args.Column = hitTest.Column; if (hitTest.Item != null) args.RowIndex = hitTest.Item.Index; args.ModifierKeys = Control.ModifierKeys; // In non-details view, we want any hit on an item to act as if it was a hit // on column 0 -- which, effectively, it was. if (args.Item != null && args.ListView.View != View.Details) { args.ColumnIndex = 0; args.Column = args.ListView.GetColumn(0); args.SubItem = args.Item.GetSubItem(0); } } /// /// This method is called every time a row is selected or deselected. This can be /// a pain if the user shift-clicks 100 rows. We override this method so we can /// trigger one event for any number of select/deselects that come from one user action /// /// protected override void OnSelectedIndexChanged(EventArgs e) { if (this.SelectionEventsSuspended) return; base.OnSelectedIndexChanged(e); // If we haven't already scheduled an event, schedule it to be triggered // By using idle time, we will wait until all select events for the same // user action have finished before triggering the event. if (!this.hasIdleHandler) { this.hasIdleHandler = true; this.RunWhenIdle(HandleApplicationIdle); } } /// /// Called when the handle of the underlying control is created /// /// protected override void OnHandleCreated(EventArgs e) { //Debug.WriteLine("OnHandleCreated"); base.OnHandleCreated(e); this.Invoke((MethodInvoker)this.OnControlCreated); } /// /// This method is called after the control has been fully created. /// protected virtual void OnControlCreated() { //Debug.WriteLine("OnControlCreated"); // Force the header control to be created when the listview handle is HeaderControl hc = this.HeaderControl; hc.WordWrap = this.HeaderWordWrap; // Make sure any overlays that are set on the hot item style take effect this.HotItemStyle = this.HotItemStyle; // Arrange for any group images to be installed after the control is created NativeMethods.SetGroupImageList(this, this.GroupImageList); this.UseExplorerTheme = this.UseExplorerTheme; this.RememberDisplayIndicies(); this.SetGroupSpacing(); if (this.VirtualMode) this.ApplyExtendedStyles(); } #endregion #region Cell editing /// /// Should we start editing the cell in response to the given mouse button event? /// /// /// protected virtual bool ShouldStartCellEdit(MouseEventArgs e) { if (this.IsCellEditing) return false; if (e.Button != MouseButtons.Left && e.Button != MouseButtons.Right) return false; if ((Control.ModifierKeys & (Keys.Shift | Keys.Control | Keys.Alt)) != 0) return false; if (this.lastMouseDownClickCount == 1 && ( this.CellEditActivation == CellEditActivateMode.SingleClick || this.CellEditActivation == CellEditActivateMode.SingleClickAlways)) return true; return (this.lastMouseDownClickCount == 2 && this.CellEditActivation == CellEditActivateMode.DoubleClick); } /// /// Handle a key press on this control. We specifically look for F2 which edits the primary column, /// or a Tab character during an edit operation, which tries to start editing on the next (or previous) cell. /// /// /// protected override bool ProcessDialogKey(Keys keyData) { if (this.IsCellEditing) return this.CellEditKeyEngine.HandleKey(this, keyData); // Treat F2 as a request to edit the primary column if (keyData == Keys.F2) { this.EditSubItem((OLVListItem)this.FocusedItem, 0); return base.ProcessDialogKey(keyData); } // Treat Ctrl-C as Copy To Clipboard. if (this.CopySelectionOnControlC && keyData == (Keys.C | Keys.Control)) { this.CopySelectionToClipboard(); return true; } // Treat Ctrl-A as Select All. if (this.SelectAllOnControlA && keyData == (Keys.A | Keys.Control)) { this.SelectAll(); return true; } return base.ProcessDialogKey(keyData); } /// /// Start an editing operation on the first editable column of the given model. /// /// /// /// /// If the model doesn't exist, or there are no editable columns, this method /// will do nothing. /// /// This will start an edit operation regardless of CellActivationMode. /// /// public virtual void EditModel(object rowModel) { OLVListItem olvItem = this.ModelToItem(rowModel); if (olvItem == null) return; for (int i = 0; i < olvItem.SubItems.Count; i++) { if (this.GetColumn(i).IsEditable) { this.StartCellEdit(olvItem, i); return; } } } /// /// Begin an edit operation on the given cell. /// /// This performs various sanity checks and passes off the real work to StartCellEdit(). /// The row to be edited /// The index of the cell to be edited public virtual void EditSubItem(OLVListItem item, int subItemIndex) { if (item == null) return; if (subItemIndex < 0 && subItemIndex >= item.SubItems.Count) return; if (this.CellEditActivation == CellEditActivateMode.None) return; if (!this.GetColumn(subItemIndex).IsEditable) return; if (!item.Enabled) return; this.StartCellEdit(item, subItemIndex); } /// /// Really start an edit operation on a given cell. The parameters are assumed to be sane. /// /// The row to be edited /// The index of the cell to be edited public virtual void StartCellEdit(OLVListItem item, int subItemIndex) { OLVColumn column = this.GetColumn(subItemIndex); Control c = this.GetCellEditor(item, subItemIndex); Rectangle cellBounds = this.CalculateCellBounds(item, subItemIndex); c.Bounds = this.CalculateCellEditorBounds(item, subItemIndex, c.PreferredSize); // Try to align the control as the column is aligned. Not all controls support this property Munger.PutProperty(c, "TextAlign", column.TextAlign); // Give the control the value from the model this.SetControlValue(c, column.GetValue(item.RowObject), column.GetStringValue(item.RowObject)); // Give the outside world the chance to munge with the process this.CellEditEventArgs = new CellEditEventArgs(column, c, cellBounds, item, subItemIndex); this.OnCellEditStarting(this.CellEditEventArgs); if (this.CellEditEventArgs.Cancel) return; // The event handler may have completely changed the control, so we need to remember it this.cellEditor = this.CellEditEventArgs.Control; this.Invalidate(); this.Controls.Add(this.cellEditor); this.ConfigureControl(); this.PauseAnimations(true); } private Control cellEditor; internal CellEditEventArgs CellEditEventArgs; /// /// Calculate the bounds of the edit control for the given item/column /// /// /// /// /// public Rectangle CalculateCellEditorBounds(OLVListItem item, int subItemIndex, Size preferredSize) { Rectangle r = CalculateCellBounds(item, subItemIndex); // Calculate the width of the cell's current contents return this.OwnerDraw ? CalculateCellEditorBoundsOwnerDrawn(item, subItemIndex, r, preferredSize) : CalculateCellEditorBoundsStandard(item, subItemIndex, r, preferredSize); } /// /// Calculate the bounds of the edit control for the given item/column, when the listview /// is being owner drawn. /// /// /// /// /// /// A rectangle that is the bounds of the cell editor protected Rectangle CalculateCellEditorBoundsOwnerDrawn(OLVListItem item, int subItemIndex, Rectangle r, Size preferredSize) { IRenderer renderer = this.View == View.Details ? this.GetCellRenderer(item.RowObject, this.GetColumn(subItemIndex)) : this.ItemRenderer; if (renderer == null) return r; using (Graphics g = this.CreateGraphics()) { return renderer.GetEditRectangle(g, r, item, subItemIndex, preferredSize); } } /// /// Calculate the bounds of the edit control for the given item/column, when the listview /// is not being owner drawn. /// /// /// /// /// /// A rectangle that is the bounds of the cell editor protected Rectangle CalculateCellEditorBoundsStandard(OLVListItem item, int subItemIndex, Rectangle cellBounds, Size preferredSize) { if (this.View == View.Tile) return cellBounds; // Center the editor vertically if (cellBounds.Height != preferredSize.Height) cellBounds.Y += (cellBounds.Height - preferredSize.Height) / 2; // Only Details view needs more processing if (this.View != View.Details) return cellBounds; // Allow for image (if there is one). int offset = 0; object imageSelector = null; if (subItemIndex == 0) imageSelector = item.ImageSelector; else { // We only check for subitem images if we are owner drawn or showing subitem images if (this.OwnerDraw || this.ShowImagesOnSubItems) imageSelector = item.GetSubItem(subItemIndex).ImageSelector; } if (this.GetActualImageIndex(imageSelector) != -1) { offset += this.SmallImageSize.Width + 2; } // Allow for checkbox if (this.CheckBoxes && this.StateImageList != null && subItemIndex == 0) { offset += this.StateImageList.ImageSize.Width + 2; } // Allow for indent (first column only) if (subItemIndex == 0 && item.IndentCount > 0) { offset += (this.SmallImageSize.Width * item.IndentCount); } // Do the adjustment if (offset > 0) { cellBounds.X += offset; cellBounds.Width -= offset; } return cellBounds; } /// /// Try to give the given value to the provided control. Fall back to assigning a string /// if the value assignment fails. /// /// A control /// The value to be given to the control /// The string to be given if the value doesn't work protected virtual void SetControlValue(Control control, Object value, String stringValue) { // Handle combobox explicitly ComboBox cb = control as ComboBox; if (cb != null) { if (cb.Created) cb.SelectedValue = value; else this.BeginInvoke(new MethodInvoker(delegate { cb.SelectedValue = value; })); return; } if (Munger.PutProperty(control, "Value", value)) return; // There wasn't a Value property, or we couldn't set it, so set the text instead try { String valueAsString = value as String; control.Text = valueAsString ?? stringValue; } catch (ArgumentOutOfRangeException) { // The value couldn't be set via the Text property. } } /// /// Setup the given control to be a cell editor /// protected virtual void ConfigureControl() { this.cellEditor.Validating += new CancelEventHandler(CellEditor_Validating); this.cellEditor.Select(); } /// /// Return the value that the given control is showing /// /// /// protected virtual Object GetControlValue(Control control) { if (control == null) return null; TextBox box = control as TextBox; if (box != null) return box.Text; ComboBox comboBox = control as ComboBox; if (comboBox != null) return comboBox.SelectedValue; CheckBox checkBox = control as CheckBox; if (checkBox != null) return checkBox.Checked; try { return control.GetType().InvokeMember("Value", BindingFlags.GetProperty, null, control, null); } catch (MissingMethodException) { // Microsoft throws this return control.Text; } catch (MissingFieldException) { // Mono throws this return control.Text; } } /// /// Called when the cell editor could be about to lose focus. Time to commit the change /// /// /// protected virtual void CellEditor_Validating(object sender, CancelEventArgs e) { this.CellEditEventArgs.Cancel = false; this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); this.OnCellEditorValidating(this.CellEditEventArgs); if (this.CellEditEventArgs.Cancel) { this.CellEditEventArgs.Control.Select(); e.Cancel = true; } else FinishCellEdit(); } /// /// Return the bounds of the given cell /// /// The row to be edited /// The index of the cell to be edited /// A Rectangle public virtual Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex) { // It seems on Win7, GetSubItemBounds() does not have the same problems with // column 0 that it did previously. // TODO - Check on XP if (this.View != View.Details) return this.GetItemRect(item.Index, ItemBoundsPortion.Label); Rectangle r = item.GetSubItemBounds(subItemIndex); r.Width -= 1; r.Height -= 1; return r; // We use ItemBoundsPortion.Label rather than ItemBoundsPortion.Item // since Label extends to the right edge of the cell, whereas Item gives just the // current text width. //return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.Label); } /// /// Return the bounds of the given cell only until the edge of the current text /// /// The row to be edited /// The index of the cell to be edited /// A Rectangle public virtual Rectangle CalculateCellTextBounds(OLVListItem item, int subItemIndex) { return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.ItemOnly); } private Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex, ItemBoundsPortion portion) { // SubItem.Bounds works for every subitem, except the first. if (subItemIndex > 0) return item.GetSubItemBounds(subItemIndex); // For non detail views, we just use the requested portion Rectangle r = this.GetItemRect(item.Index, portion); if (r.Y < -10000000 || r.Y > 10000000) { r.Y = item.Bounds.Y; } if (this.View != View.Details) return r; // Finding the bounds of cell 0 should not be a difficult task, but it is. Problems: // 1) item.SubItem[0].Bounds is always the full bounds of the entire row, not just cell 0. // 2) if column 0 has been dragged to some other position, the bounds always has a left edge of 0. // We avoid both these problems by using the position of sides the column header to calculate // the sides of the cell Point sides = NativeMethods.GetScrolledColumnSides(this, 0); r.X = sides.X + 4; r.Width = sides.Y - sides.X - 5; return r; } /// /// Calculate the visible bounds of the given column. The column's bottom edge is /// either the bottom of the last row or the bottom of the control. /// /// The bounds of the control itself /// The column /// A Rectangle /// This returns an empty rectnage if the control isn't in Details mode, /// OR has doesn't have any rows, OR if the given column is hidden. public virtual Rectangle CalculateColumnVisibleBounds(Rectangle bounds, OLVColumn column) { // Sanity checks if (column == null || this.View != System.Windows.Forms.View.Details || this.GetItemCount() == 0 || !column.IsVisible) return Rectangle.Empty; Point sides = NativeMethods.GetScrolledColumnSides(this, column.Index); if (sides.X == -1) return Rectangle.Empty; Rectangle columnBounds = new Rectangle(sides.X, bounds.Top, sides.Y - sides.X, bounds.Bottom); // Find the bottom of the last item. The column only extends to there. OLVListItem lastItem = this.GetLastItemInDisplayOrder(); if (lastItem != null) { Rectangle lastItemBounds = lastItem.Bounds; if (!lastItemBounds.IsEmpty && lastItemBounds.Bottom < columnBounds.Bottom) columnBounds.Height = lastItemBounds.Bottom - columnBounds.Top; } return columnBounds; } /// /// Return a control that can be used to edit the value of the given cell. /// /// The row to be edited /// The index of the cell to be edited /// A Control to edit the given cell protected virtual Control GetCellEditor(OLVListItem item, int subItemIndex) { OLVColumn column = this.GetColumn(subItemIndex); Object value = column.GetValue(item.RowObject) ?? this.GetFirstNonNullValue(column); // TODO: What do we do if value is still null here? // Ask the registry for an instance of the appropriate editor. // Use a default editor if the registry can't create one for us. Control editor = ObjectListView.EditorRegistry.GetEditor(item.RowObject, column, value) ?? this.MakeDefaultCellEditor(column); return editor; } /// /// Get the first non-null value of the given column. /// At most 1000 rows will be considered. /// /// /// The first non-null value, or null if no non-null values were found internal object GetFirstNonNullValue(OLVColumn column) { for (int i = 0; i < Math.Min(this.GetItemCount(), 1000); i++) { object value = column.GetValue(this.GetModelObject(i)); if (value != null) return value; } return null; } /// /// Return a TextBox that can be used as a default cell editor. /// /// What column does the cell belong to? /// protected virtual Control MakeDefaultCellEditor(OLVColumn column) { TextBox tb = new TextBox(); if (column.AutoCompleteEditor) this.ConfigureAutoComplete(tb, column); return tb; } /// /// Configure the given text box to autocomplete unique values /// from the given column. At most 1000 rows will be considered. /// /// The textbox to configure /// The column used to calculate values public void ConfigureAutoComplete(TextBox tb, OLVColumn column) { this.ConfigureAutoComplete(tb, column, 1000); } /// /// Configure the given text box to autocomplete unique values /// from the given column. At most 1000 rows will be considered. /// /// The textbox to configure /// The column used to calculate values /// Consider only this many rows public void ConfigureAutoComplete(TextBox tb, OLVColumn column, int maxRows) { // Don't consider more rows than we actually have maxRows = Math.Min(this.GetItemCount(), maxRows); // Reset any existing autocomplete tb.AutoCompleteCustomSource.Clear(); // CONSIDER: Should we use ClusteringStrategy here? // Build a list of unique values, to be used as autocomplete on the editor Dictionary alreadySeen = new Dictionary(); List values = new List(); for (int i = 0; i < maxRows; i++) { string valueAsString = column.GetStringValue(this.GetModelObject(i)); if (!String.IsNullOrEmpty(valueAsString) && !alreadySeen.ContainsKey(valueAsString)) { values.Add(valueAsString); alreadySeen[valueAsString] = true; } } tb.AutoCompleteCustomSource.AddRange(values.ToArray()); tb.AutoCompleteSource = AutoCompleteSource.CustomSource; tb.AutoCompleteMode = column.AutoCompleteEditorMode; } /// /// Stop editing a cell and throw away any changes. /// public virtual void CancelCellEdit() { if (!this.IsCellEditing) return; // Let the world know that the user has cancelled the edit operation this.CellEditEventArgs.Cancel = true; this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); this.OnCellEditFinishing(this.CellEditEventArgs); // Now cleanup the editing process this.CleanupCellEdit(false, this.CellEditEventArgs.AutoDispose); } /// /// If a cell edit is in progress, finish the edit. /// /// Returns false if the finishing process was cancelled /// (i.e. the cell editor is still on screen) /// This method does not guarantee that the editing will finish. The validation /// process can cause the finishing to be aborted. Developers should check the return value /// or use IsCellEditing property after calling this method to see if the user is still /// editing a cell. public virtual bool PossibleFinishCellEditing() { return this.PossibleFinishCellEditing(false); } /// /// If a cell edit is in progress, finish the edit. /// /// Returns false if the finishing process was cancelled /// (i.e. the cell editor is still on screen) /// This method does not guarantee that the editing will finish. The validation /// process can cause the finishing to be aborted. Developers should check the return value /// or use IsCellEditing property after calling this method to see if the user is still /// editing a cell. /// True if it is likely that another cell is going to be /// edited immediately after this cell finishes editing public virtual bool PossibleFinishCellEditing(bool expectingCellEdit) { if (!this.IsCellEditing) return true; this.CellEditEventArgs.Cancel = false; this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); this.OnCellEditorValidating(this.CellEditEventArgs); if (this.CellEditEventArgs.Cancel) return false; this.FinishCellEdit(expectingCellEdit); return true; } /// /// Finish the cell edit operation, writing changed data back to the model object /// /// This method does not trigger a Validating event, so it always finishes /// the cell edit. public virtual void FinishCellEdit() { this.FinishCellEdit(false); } /// /// Finish the cell edit operation, writing changed data back to the model object /// /// This method does not trigger a Validating event, so it always finishes /// the cell edit. /// True if it is likely that another cell is going to be /// edited immediately after this cell finishes editing public virtual void FinishCellEdit(bool expectingCellEdit) { if (!this.IsCellEditing) return; this.CellEditEventArgs.Cancel = false; this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); this.OnCellEditFinishing(this.CellEditEventArgs); // If someone doesn't cancel the editing process, write the value back into the model if (!this.CellEditEventArgs.Cancel) { this.CellEditEventArgs.Column.PutValue(this.CellEditEventArgs.RowObject, this.CellEditEventArgs.NewValue); this.RefreshItem(this.CellEditEventArgs.ListViewItem); } this.CleanupCellEdit(expectingCellEdit, this.CellEditEventArgs.AutoDispose); // Tell the world that the cell has been edited this.OnCellEditFinished(this.CellEditEventArgs); } /// /// Remove all trace of any existing cell edit operation /// /// True if it is likely that another cell is going to be /// edited immediately after this cell finishes editing /// True if the cell editor should be disposed protected virtual void CleanupCellEdit(bool expectingCellEdit, bool disposeOfCellEditor) { if (this.cellEditor == null) return; this.cellEditor.Validating -= new CancelEventHandler(CellEditor_Validating); Control soonToBeOldCellEditor = this.cellEditor; this.cellEditor = null; // Delay cleaning up the cell editor so that if we are immediately going to // start a new cell edit (because the user pressed Tab) the new cell editor // has a chance to grab the focus. Without this, the ListView gains focus // momentarily (after the cell editor is remove and before the new one is created) // causing the list's selection to flash momentarily. EventHandler toBeRun = null; toBeRun = delegate(object sender, EventArgs e) { Application.Idle -= toBeRun; this.Controls.Remove(soonToBeOldCellEditor); if (disposeOfCellEditor) soonToBeOldCellEditor.Dispose(); this.Invalidate(); if (!this.IsCellEditing) { if (this.Focused) this.Select(); this.PauseAnimations(false); } }; // We only want to delay the removal of the control if we are expecting another cell // to be edited. Otherwise, we remove the control immediately. if (expectingCellEdit) this.RunWhenIdle(toBeRun); else toBeRun(null, null); } #endregion #region Hot row and cell handling /// /// Force the hot item to be recalculated /// public virtual void ClearHotItem() { this.UpdateHotItem(new Point(-1, -1)); } /// /// Force the hot item to be recalculated /// public virtual void RefreshHotItem() { this.UpdateHotItem(this.PointToClient(Cursor.Position)); } /// /// The mouse has moved to the given pt. See if the hot item needs to be updated /// /// Where is the mouse? /// This is the main entry point for hot item handling protected virtual void UpdateHotItem(Point pt) { this.UpdateHotItem(this.OlvHitTest(pt.X, pt.Y)); } /// /// The mouse has moved to the given pt. See if the hot item needs to be updated /// /// /// This is the main entry point for hot item handling protected virtual void UpdateHotItem(OlvListViewHitTestInfo hti) { // We only need to do the work of this method when the list has hot parts // (i.e. some element whose visual appearance changes when under the mouse)? // Hot item decorations and hyperlinks are obvious, but if we have checkboxes // or buttons, those are also "hot". It's difficult to quickly detect if there are any // columns that have checkboxes or buttons, so we just abdicate responsibililty and // provide a property (UseHotControls) which lets the programmer say whether to do // the hot processing or not. if (!this.UseHotItem && !this.UseHyperlinks && !this.UseHotControls) return; int newHotRow = hti.RowIndex; int newHotColumn = hti.ColumnIndex; HitTestLocation newHotCellHitLocation = hti.HitTestLocation; HitTestLocationEx newHotCellHitLocationEx = hti.HitTestLocationEx; OLVGroup newHotGroup = hti.Group; // In non-details view, we treat any hit on a row as if it were a hit // on column 0 -- which (effectively) it is! if (newHotRow >= 0 && this.View != View.Details) newHotColumn = 0; if (this.HotRowIndex == newHotRow && this.HotColumnIndex == newHotColumn && this.HotCellHitLocation == newHotCellHitLocation && this.HotCellHitLocationEx == newHotCellHitLocationEx && this.HotGroup == newHotGroup) { return; } // Trigger the hotitem changed event HotItemChangedEventArgs args = new HotItemChangedEventArgs(); args.HotCellHitLocation = newHotCellHitLocation; args.HotCellHitLocationEx = newHotCellHitLocationEx; args.HotColumnIndex = newHotColumn; args.HotRowIndex = newHotRow; args.HotGroup = newHotGroup; args.OldHotCellHitLocation = this.HotCellHitLocation; args.OldHotCellHitLocationEx = this.HotCellHitLocationEx; args.OldHotColumnIndex = this.HotColumnIndex; args.OldHotRowIndex = this.HotRowIndex; args.OldHotGroup = this.HotGroup; this.OnHotItemChanged(args); // Update the state of the control this.HotRowIndex = newHotRow; this.HotColumnIndex = newHotColumn; this.HotCellHitLocation = newHotCellHitLocation; this.HotCellHitLocationEx = newHotCellHitLocationEx; this.HotGroup = newHotGroup; // If the event handler handled it complete, don't do anything else if (args.Handled) return; // System.Diagnostics.Debug.WriteLine(String.Format("Changed hot item: {0}", args)); this.BeginUpdate(); try { this.Invalidate(); if (args.OldHotRowIndex != -1) this.UnapplyHotItem(args.OldHotRowIndex); if (this.HotRowIndex != -1) { // Virtual lists apply hot item style when fetching their rows if (this.VirtualMode) { this.ClearCachedInfo(); this.RedrawItems(this.HotRowIndex, this.HotRowIndex, true); } else { this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, hti.Item); } } if (this.UseHotItem && this.HotItemStyleOrDefault.Overlay != null) { this.RefreshOverlays(); } } finally { this.EndUpdate(); } } /// /// Update the given row using the current hot item information /// /// protected virtual void UpdateHotRow(OLVListItem olvi) { this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, olvi); } /// /// Update the given row using the given hot item information /// /// /// /// /// protected virtual void UpdateHotRow(int rowIndex, int columnIndex, HitTestLocation hitLocation, OLVListItem olvi) { if (rowIndex < 0 || columnIndex < 0) return; // System.Diagnostics.Debug.WriteLine(String.Format("UpdateHotRow: {0}, {1}, {2}", rowIndex, columnIndex, hitLocation)); if (this.UseHyperlinks) { OLVColumn column = this.GetColumn(columnIndex); OLVListSubItem subItem = olvi.GetSubItem(columnIndex); if (column.Hyperlink && hitLocation == HitTestLocation.Text && !String.IsNullOrEmpty(subItem.Url)) { this.ApplyCellStyle(olvi, columnIndex, this.HyperlinkStyle.Over); this.Cursor = this.HyperlinkStyle.OverCursor ?? Cursors.Default; } else { this.Cursor = Cursors.Default; } } if (this.UseHotItem) { if (!olvi.Selected && olvi.Enabled) { this.ApplyRowStyle(olvi, this.HotItemStyleOrDefault); } } } /// /// Apply a style to the given row /// /// /// public virtual void ApplyRowStyle(OLVListItem olvi, IItemStyle style) { if (style == null) return; Font font = style.Font ?? olvi.Font; if (style.FontStyle != FontStyle.Regular) font = new Font(font ?? this.Font, style.FontStyle); if (!Equals(font, olvi.Font)) { if (olvi.UseItemStyleForSubItems) olvi.Font = font; else { foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) x.Font = font; } } if (!style.ForeColor.IsEmpty) { if (olvi.UseItemStyleForSubItems) olvi.ForeColor = style.ForeColor; else { foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) x.ForeColor = style.ForeColor; } } if (!style.BackColor.IsEmpty) { if (olvi.UseItemStyleForSubItems) olvi.BackColor = style.BackColor; else { foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) x.BackColor = style.BackColor; } } } /// /// Apply a style to a cell /// /// /// /// protected virtual void ApplyCellStyle(OLVListItem olvi, int columnIndex, IItemStyle style) { if (style == null) return; // Don't apply formatting to subitems when not in Details view if (this.View != View.Details && columnIndex > 0) return; olvi.UseItemStyleForSubItems = false; ListViewItem.ListViewSubItem subItem = olvi.SubItems[columnIndex]; if (style.Font != null) subItem.Font = style.Font; if (style.FontStyle != FontStyle.Regular) subItem.Font = new Font(subItem.Font ?? olvi.Font ?? this.Font, style.FontStyle); if (!style.ForeColor.IsEmpty) subItem.ForeColor = style.ForeColor; if (!style.BackColor.IsEmpty) subItem.BackColor = style.BackColor; } /// /// Remove hot item styling from the given row /// /// protected virtual void UnapplyHotItem(int index) { this.Cursor = Cursors.Default; // Virtual lists will apply the appropriate formatting when the row is fetched if (this.VirtualMode) { if (index < this.VirtualListSize) this.RedrawItems(index, index, true); } else { OLVListItem olvi = this.GetItem(index); if (olvi != null) { //this.PostProcessOneRow(index, index, olvi); this.RefreshItem(olvi); } } } #endregion #region Drag and drop /// /// /// /// protected override void OnItemDrag(ItemDragEventArgs e) { base.OnItemDrag(e); if (this.DragSource == null) return; Object data = this.DragSource.StartDrag(this, e.Button, (OLVListItem)e.Item); if (data != null) { DragDropEffects effect = this.DoDragDrop(data, this.DragSource.GetAllowedEffects(data)); this.DragSource.EndDrag(data, effect); } } /// /// /// /// protected override void OnDragEnter(DragEventArgs args) { base.OnDragEnter(args); if (this.DropSink != null) this.DropSink.Enter(args); } /// /// /// /// protected override void OnDragOver(DragEventArgs args) { base.OnDragOver(args); if (this.DropSink != null) this.DropSink.Over(args); } /// /// /// /// protected override void OnDragDrop(DragEventArgs args) { base.OnDragDrop(args); if (this.DropSink != null) this.DropSink.Drop(args); } /// /// /// /// protected override void OnDragLeave(EventArgs e) { base.OnDragLeave(e); if (this.DropSink != null) this.DropSink.Leave(); } /// /// /// /// protected override void OnGiveFeedback(GiveFeedbackEventArgs args) { base.OnGiveFeedback(args); if (this.DropSink != null) this.DropSink.GiveFeedback(args); } /// /// /// /// protected override void OnQueryContinueDrag(QueryContinueDragEventArgs args) { base.OnQueryContinueDrag(args); if (this.DropSink != null) this.DropSink.QueryContinue(args); } #endregion #region Decorations and Overlays /// /// Add the given decoration to those on this list and make it appear /// /// The decoration /// /// A decoration scrolls with the listview. An overlay stays fixed in place. /// public virtual void AddDecoration(IDecoration decoration) { if (decoration == null) return; this.Decorations.Add(decoration); this.Invalidate(); } /// /// Add the given overlay to those on this list and make it appear /// /// The overlay public virtual void AddOverlay(IOverlay overlay) { if (overlay == null) return; this.Overlays.Add(overlay); this.Invalidate(); } /// /// Draw all the decorations /// /// A Graphics /// The items that were redrawn and whose decorations should also be redrawn protected virtual void DrawAllDecorations(Graphics g, List itemsThatWereRedrawn) { g.TextRenderingHint = ObjectListView.TextRenderingHint; g.SmoothingMode = ObjectListView.SmoothingMode; Rectangle contentRectangle = this.ContentRectangle; if (this.HasEmptyListMsg && this.GetItemCount() == 0) { this.EmptyListMsgOverlay.Draw(this, g, contentRectangle); } // Let the drop sink draw whatever feedback it likes if (this.DropSink != null) { this.DropSink.DrawFeedback(g, contentRectangle); } // Draw our item and subitem decorations foreach (OLVListItem olvi in itemsThatWereRedrawn) { if (olvi.HasDecoration) { foreach (IDecoration d in olvi.Decorations) { d.ListItem = olvi; d.SubItem = null; d.Draw(this, g, contentRectangle); } } foreach (OLVListSubItem subItem in olvi.SubItems) { if (subItem.HasDecoration) { foreach (IDecoration d in subItem.Decorations) { d.ListItem = olvi; d.SubItem = subItem; d.Draw(this, g, contentRectangle); } } } if (this.SelectedRowDecoration != null && olvi.Selected && olvi.Enabled) { this.SelectedRowDecoration.ListItem = olvi; this.SelectedRowDecoration.SubItem = null; this.SelectedRowDecoration.Draw(this, g, contentRectangle); } } // Now draw the specifically registered decorations foreach (IDecoration decoration in this.Decorations) { decoration.ListItem = null; decoration.SubItem = null; decoration.Draw(this, g, contentRectangle); } // Finally, draw any hot item decoration if (this.UseHotItem) { IDecoration hotItemDecoration = this.HotItemStyleOrDefault.Decoration; if (hotItemDecoration != null) { hotItemDecoration.ListItem = this.GetItem(this.HotRowIndex); if (hotItemDecoration.ListItem == null || hotItemDecoration.ListItem.Enabled) { hotItemDecoration.SubItem = hotItemDecoration.ListItem == null ? null : hotItemDecoration.ListItem.GetSubItem(this.HotColumnIndex); hotItemDecoration.Draw(this, g, contentRectangle); } } } // If we are in design mode, we don't want to use the glass panels, // so we draw the background overlays here if (this.DesignMode) { foreach (IOverlay overlay in this.Overlays) { overlay.Draw(this, g, contentRectangle); } } } /// /// Is the given decoration shown on this list /// /// The overlay public virtual bool HasDecoration(IDecoration decoration) { return this.Decorations.Contains(decoration); } /// /// Is the given overlay shown on this list? /// /// The overlay public virtual bool HasOverlay(IOverlay overlay) { return this.Overlays.Contains(overlay); } /// /// Hide any overlays. /// /// /// This is only a temporary hiding -- the overlays will be shown /// the next time the ObjectListView redraws. /// public virtual void HideOverlays() { foreach (GlassPanelForm glassPanel in this.glassPanels) { glassPanel.HideGlass(); } } /// /// Create and configure the empty list msg overlay /// protected virtual void InitializeEmptyListMsgOverlay() { TextOverlay overlay = new TextOverlay(); overlay.Alignment = System.Drawing.ContentAlignment.MiddleCenter; overlay.TextColor = SystemColors.ControlDarkDark; overlay.BackColor = Color.BlanchedAlmond; overlay.BorderColor = SystemColors.ControlDark; overlay.BorderWidth = 2.0f; this.EmptyListMsgOverlay = overlay; } /// /// Initialize the standard image and text overlays /// protected virtual void InitializeStandardOverlays() { this.OverlayImage = new ImageOverlay(); this.AddOverlay(this.OverlayImage); this.OverlayText = new TextOverlay(); this.AddOverlay(this.OverlayText); } /// /// Make sure that any overlays are visible. /// public virtual void ShowOverlays() { // If we shouldn't show overlays, then don't create glass panels if (!this.ShouldShowOverlays()) return; // Make sure that each overlay has its own glass panels if (this.Overlays.Count != this.glassPanels.Count) { foreach (IOverlay overlay in this.Overlays) { GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); if (glassPanel == null) { glassPanel = new GlassPanelForm(); glassPanel.Bind(this, overlay); this.glassPanels.Add(glassPanel); } } } foreach (GlassPanelForm glassPanel in this.glassPanels) { glassPanel.ShowGlass(); } } private bool ShouldShowOverlays() { // If we are in design mode, we dont show the overlays if (this.DesignMode) return false; // If we are explicitly not using overlays, also don't show them if (!this.UseOverlays) return false; // If there are no overlays, guess... if (!this.HasOverlays) return false; // If we don't have 32-bit display, alpha blending doesn't work, so again, no overlays // TODO: This should actually figure out which screen(s) the control is on, and make sure // that each one is 32-bit. if (Screen.PrimaryScreen.BitsPerPixel < 32) return false; // Finally, we can show the overlays return true; } private GlassPanelForm FindGlassPanelForOverlay(IOverlay overlay) { return this.glassPanels.Find(delegate(GlassPanelForm x) { return x.Overlay == overlay; }); } /// /// Refresh the display of the overlays /// public virtual void RefreshOverlays() { foreach (GlassPanelForm glassPanel in this.glassPanels) { glassPanel.Invalidate(); } } /// /// Refresh the display of just one overlays /// public virtual void RefreshOverlay(IOverlay overlay) { GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); if (glassPanel != null) glassPanel.Invalidate(); } /// /// Remove the given decoration from this list /// /// The decoration to remove public virtual void RemoveDecoration(IDecoration decoration) { if (decoration == null) return; this.Decorations.Remove(decoration); this.Invalidate(); } /// /// Remove the given overlay to those on this list /// /// The overlay public virtual void RemoveOverlay(IOverlay overlay) { if (overlay == null) return; this.Overlays.Remove(overlay); GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); if (glassPanel != null) { this.glassPanels.Remove(glassPanel); glassPanel.Unbind(); glassPanel.Dispose(); } } #endregion #region Filtering /// /// Create a filter that will enact all the filtering currently installed /// on the visible columns. /// public virtual IModelFilter CreateColumnFilter() { List filters = new List(); foreach (OLVColumn column in this.Columns) { IModelFilter filter = column.ValueBasedFilter; if (filter != null) filters.Add(filter); } return (filters.Count == 0) ? null : new CompositeAllFilter(filters); } /// /// Do the actual work of filtering /// /// /// /// /// virtual protected IEnumerable FilterObjects(IEnumerable originalObjects, IModelFilter aModelFilter, IListFilter aListFilter) { // Being cautious originalObjects = originalObjects ?? new ArrayList(); // Tell the world to filter the objects. If they do so, don't do anything else // ReSharper disable PossibleMultipleEnumeration FilterEventArgs args = new FilterEventArgs(originalObjects); this.OnFilter(args); if (args.FilteredObjects != null) return args.FilteredObjects; // Apply a filter to the list as a whole if (aListFilter != null) originalObjects = aListFilter.Filter(originalObjects); // Apply the object filter if there is one if (aModelFilter != null) { ArrayList filteredObjects = new ArrayList(); foreach (object model in originalObjects) { if (aModelFilter.Filter(model)) filteredObjects.Add(model); } originalObjects = filteredObjects; } return originalObjects; // ReSharper restore PossibleMultipleEnumeration } /// /// 删除所有列筛选。 /// public virtual void ResetColumnFiltering() { foreach (OLVColumn column in this.Columns) { column.ValuesChosenForFiltering.Clear(); } this.UpdateColumnFiltering(); } /// /// 根据每列中定义的值筛选更新此ObjectListView的筛选 /// public virtual void UpdateColumnFiltering() { //List filters = new List(); //IModelFilter columnFilter = this.CreateColumnFilter(); //if (columnFilter != null) // filters.Add(columnFilter); //if (this.AdditionalFilter != null) // filters.Add(this.AdditionalFilter); //this.ModelFilter = filters.Count == 0 ? null : new CompositeAllFilter(filters); if (this.AdditionalFilter == null) this.ModelFilter = this.CreateColumnFilter(); else { IModelFilter columnFilter = this.CreateColumnFilter(); if (columnFilter == null) this.ModelFilter = this.AdditionalFilter; else { List filters = new List(); filters.Add(columnFilter); filters.Add(this.AdditionalFilter); this.ModelFilter = new CompositeAllFilter(filters); } } } /// /// When some setting related to filtering changes, this method is called. /// protected virtual void UpdateFiltering() { this.BuildList(true); } /// /// 使用当前安装的模型过滤器更新所有渲染器 /// protected virtual void NotifyNewModelFilter() { IFilterAwareRenderer filterAware = this.DefaultRenderer as IFilterAwareRenderer; if (filterAware != null) filterAware.Filter = this.ModelFilter; foreach (OLVColumn column in this.AllColumns) { filterAware = column.Renderer as IFilterAwareRenderer; if (filterAware != null) filterAware.Filter = this.ModelFilter; } } #endregion #region Persistent check state /// /// 获取给定模型的复选框状态。 /// /// The model /// 模型的复选框状态。默认为未选中。 protected virtual CheckState GetPersistentCheckState(object model) { CheckState state; if (model != null && this.CheckStateMap.TryGetValue(model, out state)) return state; return CheckState.Unchecked; } /// /// 设置给定模型对象的复选框状态 /// /// The model to be remembered /// The model's checkedness /// The state given to the method protected virtual CheckState SetPersistentCheckState(object model, CheckState state) { if (model == null) return CheckState.Unchecked; this.CheckStateMap[model] = state; return state; } /// /// 忽略任何持久复选框状态 /// protected virtual void ClearPersistentCheckState() { this.CheckStateMap = null; } #endregion #region Implementation variables private bool isOwnerOfObjects; // does this ObjectListView own the Objects collection? private bool hasIdleHandler; // has an Idle handler already been installed? private bool hasResizeColumnsHandler; // has an idle handler been installed which will handle column resizing? private bool isInWmPaintEvent; // is a WmPaint event currently being handled? private bool shouldDoCustomDrawing; // should the list do its custom drawing? private bool isMarqueSelecting; // Is a marque selection in progress? private int suspendSelectionEventCount; // How many unmatched SuspendSelectionEvents() calls have been made? private readonly List glassPanels = new List(); // The transparent panel that draws overlays private Dictionary visitedUrlMap = new Dictionary(); // Which urls have been visited? // TODO //private CheckBoxSettings checkBoxSettings = new CheckBoxSettings(); #endregion } }