|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Runtime.InteropServices;
using System.Windows.Forms.Layout;
using System.Windows.Forms.VisualStyles;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
using static System.Windows.Forms.TreeNode;
namespace System.Windows.Forms;
/// <summary>
/// Displays a hierarchical list of items, or nodes. Each
/// node includes a caption and an optional bitmap. The user can select a node. If
/// it has sub-nodes, the user can collapse or expand the node.
/// </summary>
[DefaultProperty(nameof(Nodes))]
[DefaultEvent(nameof(AfterSelect))]
[Docking(DockingBehavior.Ask)]
[Designer($"System.Windows.Forms.Design.TreeViewDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionTreeView))]
public partial class TreeView : Control
{
private const int MaxIndent = 32000; // Maximum allowable TreeView indent
private const string BackSlash = "\\";
private const int DefaultTreeViewIndent = 19;
private DrawTreeNodeEventHandler? _onDrawNode;
private NodeLabelEditEventHandler? _onBeforeLabelEdit;
private NodeLabelEditEventHandler? _onAfterLabelEdit;
private TreeViewCancelEventHandler? _onBeforeCheck;
private TreeViewEventHandler? _onAfterCheck;
private TreeViewCancelEventHandler? _onBeforeCollapse;
private TreeViewEventHandler? _onAfterCollapse;
private TreeViewCancelEventHandler? _onBeforeExpand;
private TreeViewEventHandler? _onAfterExpand;
private TreeViewCancelEventHandler? _onBeforeSelect;
private TreeViewEventHandler? _onAfterSelect;
private ItemDragEventHandler? _onItemDrag;
private TreeNodeMouseHoverEventHandler? _onNodeMouseHover;
private EventHandler? _onRightToLeftLayoutChanged;
internal TreeNode? _selectedNode;
private ImageList.Indexer? _imageIndexer;
private ImageList.Indexer? _selectedImageIndexer;
private bool _setOddHeight;
private TreeNode? _prevHoveredNode;
private bool _hoveredAlready;
private bool _rightToLeftLayout;
private nint _mouseDownNode = 0; // ensures we fire nodeClick on the correct node
private const int TREEVIEWSTATE_hideSelection = 0x00000001;
private const int TREEVIEWSTATE_labelEdit = 0x00000002;
private const int TREEVIEWSTATE_scrollable = 0x00000004;
private const int TREEVIEWSTATE_checkBoxes = 0x00000008;
private const int TREEVIEWSTATE_showLines = 0x00000010;
private const int TREEVIEWSTATE_showPlusMinus = 0x00000020;
private const int TREEVIEWSTATE_showRootLines = 0x00000040;
private const int TREEVIEWSTATE_sorted = 0x00000080;
private const int TREEVIEWSTATE_hotTracking = 0x00000100;
private const int TREEVIEWSTATE_fullRowSelect = 0x00000200;
private const int TREEVIEWSTATE_showNodeToolTips = 0x00000400;
private const int TREEVIEWSTATE_doubleclickFired = 0x00000800;
private const int TREEVIEWSTATE_mouseUpFired = 0x00001000;
private const int TREEVIEWSTATE_showTreeViewContextMenu = 0x00002000;
private const int TREEVIEWSTATE_lastControlValidated = 0x00004000;
private const int TREEVIEWSTATE_stopResizeWindowMsgs = 0x00008000;
private const int TREEVIEWSTATE_ignoreSelects = 0x00010000;
private const int TREEVIEWSTATE_doubleBufferedPropertySet = 0x00020000;
// PERF: take all the bools and put them into a state variable
private Collections.Specialized.BitVector32 _treeViewState; // see TREEVIEWSTATE_ constants above
private static bool s_isScalingInitialized;
private static Size? s_stateImageSize;
private static Size? StateImageSize
{
get
{
if (!s_isScalingInitialized)
{
const int LogicalStateImageSize = 16;
int imageSize = ScaleHelper.ScaleToInitialSystemDpi(LogicalStateImageSize);
s_stateImageSize = new(imageSize, imageSize);
s_isScalingInitialized = true;
}
return s_stateImageSize;
}
}
internal ImageList.Indexer ImageIndexer
{
get
{
_imageIndexer ??= new ImageList.Indexer();
_imageIndexer.ImageList = ImageList;
return _imageIndexer;
}
}
internal ImageList.Indexer SelectedImageIndexer
{
get
{
_selectedImageIndexer ??= new ImageList.Indexer();
_selectedImageIndexer.ImageList = ImageList;
return _selectedImageIndexer;
}
}
private ImageList? _imageList;
private int _indent = -1;
private int _itemHeight = -1;
private string _pathSeparator = BackSlash;
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
internal TreeNodeCollection? _nodes;
internal TreeNode? _editNode;
internal TreeNode _root;
internal Dictionary<IntPtr, TreeNode> _nodesByHandle = [];
internal bool _nodesCollectionClear; // this is set when the treeNodeCollection is getting cleared and used by TreeView
internal TreeViewLabelEditNativeWindow? _labelEdit;
private MouseButtons _downButton;
private TreeViewDrawMode _drawMode = TreeViewDrawMode.Normal;
// Properties newly added to TreeView....
private ImageList? _internalStateImageList;
private TreeNode? _topNode;
private ImageList? _stateImageList;
private Color _lineColor;
private string? _controlToolTipText;
// Sorting
private IComparer? _treeViewNodeSorter;
// Events
private TreeNodeMouseClickEventHandler? _onNodeMouseClick;
private TreeNodeMouseClickEventHandler? _onNodeMouseDoubleClick;
private ToolTipBuffer _toolTipBuffer;
/// <summary>
/// Creates a TreeView control
/// </summary>
public TreeView()
: base()
{
_treeViewState = new Collections.Specialized.BitVector32(
TREEVIEWSTATE_showRootLines |
TREEVIEWSTATE_showPlusMinus |
TREEVIEWSTATE_showLines |
TREEVIEWSTATE_scrollable |
TREEVIEWSTATE_hideSelection);
_root = new TreeNode(this);
// TreeView must always have an ImageIndex.
SelectedImageIndexer.Index = 0;
ImageIndexer.Index = 0;
SetStyle(ControlStyles.UserPaint, false);
SetStyle(ControlStyles.StandardClick, false);
SetStyle(ControlStyles.UseTextForAccessibility, false);
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
#pragma warning restore WFO5001
}
internal override void ReleaseUiaProvider(HWND handle)
{
foreach (TreeNode rootNode in Nodes)
{
foreach (TreeNode node in rootNode.GetSelfAndChildNodes())
{
node.ReleaseUiaProvider();
}
}
base.ReleaseUiaProvider(handle);
}
/// <summary>
/// The background color for this control. Specifying null for
/// this parameter sets the
/// control's background color to its parent's background color.
/// </summary>
public override Color BackColor
{
get => ShouldSerializeBackColor() ? base.BackColor : SystemColors.Window;
set
{
base.BackColor = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETBKCOLOR, 0, BackColor.ToWin32());
// This is to get around a problem in the comctl control where the lines
// connecting nodes don't get the new BackColor. This messages forces
// reconstruction of the line bitmaps without changing anything else.
PInvokeCore.SendMessage(this, PInvoke.TVM_SETINDENT, (WPARAM)Indent);
}
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override Image? BackgroundImage
{
get => base.BackgroundImage;
set => base.BackgroundImage = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageChanged
{
add => base.BackgroundImageChanged += value;
remove => base.BackgroundImageChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override ImageLayout BackgroundImageLayout
{
get => base.BackgroundImageLayout;
set => base.BackgroundImageLayout = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageLayoutChanged
{
add => base.BackgroundImageLayoutChanged += value;
remove => base.BackgroundImageLayoutChanged -= value;
}
/// <summary>
/// The border style of the window.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(BorderStyle.Fixed3D)]
[DispId(PInvokeCore.DISPID_BORDERSTYLE)]
[SRDescription(nameof(SR.borderStyleDescr))]
public BorderStyle BorderStyle
{
get => _borderStyle;
set
{
if (_borderStyle != value)
{
SourceGenerated.EnumValidator.Validate(value);
_borderStyle = value;
UpdateStyles();
}
}
}
/// <summary>
/// The value of the CheckBoxes property. The CheckBoxes
/// property determines if check boxes are shown next to node in the
/// tree view.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewCheckBoxesDescr))]
public bool CheckBoxes
{
get => _treeViewState[TREEVIEWSTATE_checkBoxes];
set
{
if (CheckBoxes != value)
{
_treeViewState[TREEVIEWSTATE_checkBoxes] = value;
if (IsHandleCreated)
{
if (CheckBoxes)
{
UpdateStyles();
}
else
{
// Going from true to false requires recreation
// Reset the Checked state after setting the checkboxes (this was Everett behavior)
// The implementation of the TreeNode.Checked property has changed in Whidbey
// So we need to explicit set the Checked state to false to keep the everett behavior.
UpdateCheckedState(_root, false);
RecreateHandle();
}
}
}
}
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ClassName = PInvoke.WC_TREEVIEW;
// Keep the scrollbar if we are just updating styles...
//
if (IsHandleCreated)
{
int currentStyle = unchecked((int)((long)PInvokeCore.GetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE)));
cp.Style |= currentStyle & (int)(WINDOW_STYLE.WS_HSCROLL | WINDOW_STYLE.WS_VSCROLL);
}
switch (_borderStyle)
{
case BorderStyle.Fixed3D:
cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
break;
case BorderStyle.FixedSingle:
cp.Style |= (int)WINDOW_STYLE.WS_BORDER;
break;
}
if (!Scrollable)
{
cp.Style |= (int)PInvoke.LVS_NOSCROLL;
}
if (!HideSelection)
{
cp.Style |= (int)PInvoke.TVS_SHOWSELALWAYS;
}
if (LabelEdit)
{
cp.Style |= (int)PInvoke.TVS_EDITLABELS;
}
if (ShowLines)
{
cp.Style |= (int)PInvoke.TVS_HASLINES;
}
if (ShowPlusMinus)
{
cp.Style |= (int)PInvoke.TVS_HASBUTTONS;
}
if (ShowRootLines)
{
cp.Style |= (int)PInvoke.TVS_LINESATROOT;
}
if (HotTracking)
{
cp.Style |= (int)PInvoke.TVS_TRACKSELECT;
}
if (FullRowSelect)
{
cp.Style |= (int)PInvoke.TVS_FULLROWSELECT;
}
if (_setOddHeight)
{
cp.Style |= (int)PInvoke.TVS_NONEVENHEIGHT;
}
// Don't set TVS_CHECKBOXES here if the window isn't created yet.
// See OnHandleCreated for explanation
if (ShowNodeToolTips && IsHandleCreated && !DesignMode)
{
cp.Style |= (int)PInvoke.TVS_INFOTIP;
}
// Don't set TVS_CHECKBOXES here if the window isn't created yet.
// See OnHandleCreated for explanation
if (CheckBoxes && IsHandleCreated)
{
cp.Style |= (int)PInvoke.TVS_CHECKBOXES;
}
// Don't call IsMirrored from CreateParams. That will lead to some nasty problems, since
// IsMirrored ends up calling CreateParams - you dig!
if (RightToLeft == RightToLeft.Yes)
{
if (RightToLeftLayout)
{
// We want to turn on mirroring for TreeView explicitly.
cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_LAYOUTRTL;
// Don't need these styles when mirroring is turned on.
cp.ExStyle &= ~(int)(WINDOW_EX_STYLE.WS_EX_RTLREADING | WINDOW_EX_STYLE.WS_EX_RIGHT | WINDOW_EX_STYLE.WS_EX_LEFTSCROLLBAR);
}
else
{
cp.Style |= (int)PInvoke.TVS_RTLREADING;
}
}
return cp;
}
}
/// <summary>
/// Deriving classes can override this to configure a default size for their control.
/// This is more efficient than setting the size in the control's constructor.
/// </summary>
protected override Size DefaultSize => new(121, 97);
/// <summary>
/// This property is overridden and hidden from statement completion
/// on controls that are based on Win32 Native Controls.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override bool DoubleBuffered
{
get => base.DoubleBuffered;
set
{
if (DoubleBuffered != value)
{
base.DoubleBuffered = value;
_treeViewState[TREEVIEWSTATE_doubleBufferedPropertySet] = true;
UpdateTreeViewExtendedStyles();
}
}
}
/// <summary>
/// The current foreground color for this control, which is the
/// color the control uses to draw its text.
/// </summary>
public override Color ForeColor
{
get => ShouldSerializeForeColor() ? base.ForeColor : SystemColors.WindowText;
set
{
base.ForeColor = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETTEXTCOLOR, 0, ForeColor.ToWin32());
}
}
}
/// <summary>
/// Determines whether the selection highlight spans across the width of the TreeView.
/// This property will have no effect if ShowLines is true.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewFullRowSelectDescr))]
public bool FullRowSelect
{
get => _treeViewState[TREEVIEWSTATE_fullRowSelect];
set
{
if (FullRowSelect != value)
{
_treeViewState[TREEVIEWSTATE_fullRowSelect] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// The HideSelection property specifies whether the selected node will
/// be highlighted even when the TreeView loses focus.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TreeViewHideSelectionDescr))]
public bool HideSelection
{
get => _treeViewState[TREEVIEWSTATE_hideSelection];
set
{
if (HideSelection != value)
{
_treeViewState[TREEVIEWSTATE_hideSelection] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// The value of the HotTracking property. The HotTracking
/// property determines if nodes are highlighted as the mousePointer
/// passes over them.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewHotTrackingDescr))]
public bool HotTracking
{
get => _treeViewState[TREEVIEWSTATE_hotTracking];
set
{
if (HotTracking != value)
{
_treeViewState[TREEVIEWSTATE_hotTracking] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// The default image index for nodes in the tree view.
/// </summary>
[DefaultValue(ImageList.Indexer.DefaultIndex)]
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[RefreshProperties(RefreshProperties.Repaint)]
[TypeConverter(typeof(NoneExcludedImageIndexConverter))]
[Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
[SRDescription(nameof(SR.TreeViewImageIndexDescr))]
[RelatedImageList("ImageList")]
public int ImageIndex
{
get
{
if (_imageList is null)
{
return ImageList.Indexer.DefaultIndex;
}
if (ImageIndexer.Index >= _imageList.Images.Count)
{
return Math.Max(0, _imageList.Images.Count - 1);
}
return ImageIndexer.Index;
}
set
{
// If (none) is selected in the image index editor, we'll just adjust this to
// mean image index 0. This is because a treeView must always have an image index -
// even if no imageList exists we want the image index to be 0.
if (value == ImageList.Indexer.DefaultIndex)
{
value = 0;
}
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (ImageIndexer.Index != value)
{
ImageIndexer.Index = value;
if (IsHandleCreated)
{
RecreateHandle();
}
}
}
}
/// <summary>
/// The default image index for nodes in the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[TypeConverter(typeof(ImageKeyConverter))]
[Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
[DefaultValue(ImageList.Indexer.DefaultKey)]
[RefreshProperties(RefreshProperties.Repaint)]
[SRDescription(nameof(SR.TreeViewImageKeyDescr))]
[RelatedImageList("ImageList")]
[AllowNull]
public string ImageKey
{
get => ImageIndexer.Key;
set
{
if (ImageIndexer.Key != value)
{
ImageIndexer.Key = value;
if (string.IsNullOrEmpty(value) || value.Equals(SR.toStringNone))
{
ImageIndex = (ImageList is not null) ? 0 : ImageList.Indexer.DefaultIndex;
}
if (IsHandleCreated)
{
RecreateHandle();
}
}
}
}
/// <summary>
/// Returns the image list control that is bound to the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(null)]
[SRDescription(nameof(SR.TreeViewImageListDescr))]
[RefreshProperties(RefreshProperties.Repaint)]
public ImageList? ImageList
{
get => _imageList;
set
{
if (value != _imageList)
{
DetachImageListHandlers();
_imageList = value;
AttachImageListHandlers();
// Update TreeView's images
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, 0, value is null ? 0 : value.Handle);
if (StateImageList is not null && StateImageList.Images.Count > 0 && _internalStateImageList is not null)
{
SetStateImageList(_internalStateImageList.Handle);
}
}
UpdateCheckedState(_root, true);
}
}
}
private void AttachImageListHandlers()
{
if (_imageList is not null)
{
// NOTE: any handlers added here should be removed in DetachImageListHandlers
_imageList.RecreateHandle += ImageListRecreateHandle;
_imageList.Disposed += DetachImageList;
_imageList.ChangeHandle += ImageListChangedHandle;
}
}
private void DetachImageListHandlers()
{
if (_imageList is not null)
{
_imageList.RecreateHandle -= ImageListRecreateHandle;
_imageList.Disposed -= DetachImageList;
_imageList.ChangeHandle -= ImageListChangedHandle;
}
}
private void AttachStateImageListHandlers()
{
if (_stateImageList is not null)
{
// NOTE: any handlers added here should be removed in DetachStateImageListHandlers
_stateImageList.RecreateHandle += StateImageListRecreateHandle;
_stateImageList.Disposed += DetachStateImageList;
_stateImageList.ChangeHandle += StateImageListChangedHandle;
}
}
private void DetachStateImageListHandlers()
{
if (_stateImageList is not null)
{
_stateImageList.RecreateHandle -= StateImageListRecreateHandle;
_stateImageList.Disposed -= DetachStateImageList;
_stateImageList.ChangeHandle -= StateImageListChangedHandle;
}
}
/// <summary>
/// Returns the state image list control that is bound to the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(null)]
[SRDescription(nameof(SR.TreeViewStateImageListDescr))]
public ImageList? StateImageList
{
get => _stateImageList;
set
{
if (value != _stateImageList)
{
DetachStateImageListHandlers();
_stateImageList = value;
AttachStateImageListHandlers();
// Update TreeView's images.
if (IsHandleCreated)
{
UpdateNativeStateImageList();
// We need to update the checks
// and stateImage value for each node.
UpdateCheckedState(_root, true);
if ((_stateImageList is null || _stateImageList.Images.Count == 0) && CheckBoxes)
{
// Requires Handle Recreate to force on the checkBoxes and states..
RecreateHandle();
}
else
{
// The TreeView shows up the state imageList after sending this message even if the nodes don't have any stateImageIndex set.
// In order to avoid that we refresh nodes which would "reset" the images to none.
// This causes flicker but gives us the right behavior
RefreshNodes();
}
}
}
}
}
/// <summary>
/// The indentation level in pixels.
/// </summary>
[Localizable(true)]
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewIndentDescr))]
public int Indent
{
get
{
if (_indent != -1)
{
return _indent;
}
else if (IsHandleCreated)
{
return (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETINDENT);
}
return DefaultTreeViewIndent;
}
set
{
if (_indent != value)
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MaxIndent);
_indent = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETINDENT, (WPARAM)value);
_indent = (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETINDENT);
}
}
}
}
/// <summary>
/// The height of every item in the tree view, in pixels.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.TreeViewItemHeightDescr))]
public int ItemHeight
{
get
{
if (_itemHeight != -1)
{
return _itemHeight;
}
if (IsHandleCreated)
{
return (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETITEMHEIGHT);
}
else
{
if (CheckBoxes && (DrawMode == TreeViewDrawMode.OwnerDrawAll))
{
return Math.Max(16, FontHeight + 3);
}
return FontHeight + 3;
}
}
set
{
if (_itemHeight != value)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, short.MaxValue);
_itemHeight = value;
if (IsHandleCreated)
{
if (_itemHeight % 2 != 0)
{
_setOddHeight = true;
try
{
RecreateHandle();
}
finally
{
_setOddHeight = false;
}
}
PInvokeCore.SendMessage(this, PInvoke.TVM_SETITEMHEIGHT, (WPARAM)value);
_itemHeight = (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETITEMHEIGHT);
}
}
}
}
internal ToolTip KeyboardToolTip { get; } = new();
/// <summary>
/// The LabelEdit property determines if the label text
/// of nodes in the tree view is editable.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewLabelEditDescr))]
public bool LabelEdit
{
get => _treeViewState[TREEVIEWSTATE_labelEdit];
set
{
if (LabelEdit != value)
{
_treeViewState[TREEVIEWSTATE_labelEdit] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// This is the color of the lines that connect the nodes of the TreeView.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewLineColorDescr))]
[DefaultValue(typeof(Color), "Black")]
public Color LineColor
{
get
{
if (IsHandleCreated)
{
int intColor = (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETLINECOLOR);
return ColorTranslator.FromWin32(intColor);
}
return _lineColor;
}
set
{
if (_lineColor != value)
{
_lineColor = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETLINECOLOR, 0, _lineColor.ToWin32());
}
}
}
}
/// <summary>
/// The collection of nodes associated with this TreeView control
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Localizable(true)]
[SRDescription(nameof(SR.TreeViewNodesDescr))]
[MergableProperty(false)]
public TreeNodeCollection Nodes
{
get
{
_nodes ??= new TreeNodeCollection(_root);
return _nodes;
}
}
/// <summary>
/// Indicates the drawing mode for the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(TreeViewDrawMode.Normal)]
[SRDescription(nameof(SR.TreeViewDrawModeDescr))]
public TreeViewDrawMode DrawMode
{
get => _drawMode;
set
{
// valid values are 0x0 to 0x2
SourceGenerated.EnumValidator.Validate(value);
if (_drawMode != value)
{
_drawMode = value;
Invalidate();
// We need to invalidate when the Control resizes when the we support custom draw.
if (DrawMode == TreeViewDrawMode.OwnerDrawAll)
{
SetStyle(ControlStyles.ResizeRedraw, true);
}
}
}
}
/// <summary>
/// The delimiter string used by TreeNode.getFullPath().
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue("\\")]
[SRDescription(nameof(SR.TreeViewPathSeparatorDescr))]
public string PathSeparator
{
get => _pathSeparator;
set => _pathSeparator = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Padding Padding
{
get => base.Padding;
set => base.Padding = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? PaddingChanged
{
add => base.PaddingChanged += value;
remove => base.PaddingChanged -= value;
}
/// <summary>
/// This is used for international applications where the language is written from RightToLeft.
/// When this property is true, and the RightToLeft is true, mirroring will be turned on on
/// the form, and control placement and text will be from right to left.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue(false)]
[SRDescription(nameof(SR.ControlRightToLeftLayoutDescr))]
public virtual bool RightToLeftLayout
{
get => _rightToLeftLayout;
set
{
if (value != _rightToLeftLayout)
{
_rightToLeftLayout = value;
using (new LayoutTransaction(this, this, PropertyNames.RightToLeftLayout))
{
OnRightToLeftLayoutChanged(EventArgs.Empty);
}
}
}
}
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TreeViewScrollableDescr))]
public bool Scrollable
{
get => _treeViewState[TREEVIEWSTATE_scrollable];
set
{
if (Scrollable != value)
{
_treeViewState[TREEVIEWSTATE_scrollable] = value;
RecreateHandle();
}
}
}
/// <summary>
/// The image index that a node will display when selected.
/// The index applies to the ImageList referred to by the imageList property,
/// </summary>
[DefaultValue(ImageList.Indexer.DefaultIndex)]
[SRCategory(nameof(SR.CatBehavior))]
[TypeConverter(typeof(NoneExcludedImageIndexConverter))]
[Localizable(true)]
[Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
[SRDescription(nameof(SR.TreeViewSelectedImageIndexDescr))]
[RelatedImageList("ImageList")]
public int SelectedImageIndex
{
get
{
if (_imageList is null)
{
return ImageList.Indexer.DefaultIndex;
}
if (SelectedImageIndexer.Index >= _imageList.Images.Count)
{
return Math.Max(0, _imageList.Images.Count - 1);
}
return SelectedImageIndexer.Index;
}
set
{
// If (none) is selected in the image index editor, we'll just adjust this to
// mean image index 0. This is because a treeView must always have an image index -
// even if no imageList exists we want the image index to be 0.
//
if (value == ImageList.Indexer.DefaultIndex)
{
value = 0;
}
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (SelectedImageIndexer.Index != value)
{
SelectedImageIndexer.Index = value;
if (IsHandleCreated)
{
RecreateHandle();
}
}
}
}
/// <summary>
/// The default image index for nodes in the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[TypeConverter(typeof(ImageKeyConverter))]
[Editor($"System.Windows.Forms.Design.ImageIndexEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
[DefaultValue(ImageList.Indexer.DefaultKey)]
[RefreshProperties(RefreshProperties.Repaint)]
[SRDescription(nameof(SR.TreeViewSelectedImageKeyDescr))]
[RelatedImageList("ImageList")]
[AllowNull]
public string SelectedImageKey
{
get => SelectedImageIndexer.Key;
set
{
if (SelectedImageIndexer.Key != value)
{
SelectedImageIndexer.Key = value;
if (string.IsNullOrEmpty(value) || value.Equals(SR.toStringNone))
{
SelectedImageIndex = (ImageList is not null) ? 0 : ImageList.Indexer.DefaultIndex;
}
if (IsHandleCreated)
{
RecreateHandle();
}
}
}
}
/// <summary>
/// The currently selected tree node, or null if nothing is selected.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TreeViewSelectedNodeDescr))]
public TreeNode? SelectedNode
{
get
{
if (IsHandleCreated)
{
IntPtr hItem = PInvokeCore.SendMessage(this, PInvoke.TVM_GETNEXTITEM, (WPARAM)PInvoke.TVGN_CARET);
if (hItem == IntPtr.Zero)
{
return null;
}
return NodeFromHandle(hItem);
}
else if (_selectedNode is not null && _selectedNode.TreeView == this)
{
return _selectedNode;
}
else
{
return null;
}
}
set
{
if (IsHandleCreated && (value is null || value.TreeView == this))
{
// This class invariant is not quite correct -- if the selected node does not belong to this TreeView,
// selectedNode is not null even though the handle is created. We will call set_SelectedNode
// to inform the handle that the selected node has been added to the TreeView.
Debug.Assert(_selectedNode is null || _selectedNode.TreeView != this, "handle is created, but we're still caching selectedNode");
nint hnode = (value is null ? 0 : value.Handle);
PInvokeCore.SendMessage(this, PInvoke.TVM_SELECTITEM, (WPARAM)PInvoke.TVGN_CARET, (LPARAM)hnode);
_selectedNode = null;
}
else
{
_selectedNode = value;
}
}
}
/// <summary>
/// The ShowLines property determines if lines are drawn between
/// nodes in the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TreeViewShowLinesDescr))]
public bool ShowLines
{
get => _treeViewState[TREEVIEWSTATE_showLines];
set
{
if (ShowLines != value)
{
_treeViewState[TREEVIEWSTATE_showLines] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// The ShowLines property determines whether or not the tooltips will be displayed on the nodes
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewShowShowNodeToolTipsDescr))]
public bool ShowNodeToolTips
{
get => _treeViewState[TREEVIEWSTATE_showNodeToolTips];
set
{
if (ShowNodeToolTips != value)
{
_treeViewState[TREEVIEWSTATE_showNodeToolTips] = value;
if (ShowNodeToolTips)
{
RecreateHandle();
}
}
}
}
/// <summary>
/// The ShowPlusMinus property determines if the "plus/minus"
/// expand button is shown next to tree nodes that have children.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TreeViewShowPlusMinusDescr))]
public bool ShowPlusMinus
{
get => _treeViewState[TREEVIEWSTATE_showPlusMinus];
set
{
if (ShowPlusMinus != value)
{
_treeViewState[TREEVIEWSTATE_showPlusMinus] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// Determines if lines are draw between nodes at the root of
/// the tree view.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TreeViewShowRootLinesDescr))]
public bool ShowRootLines
{
get => _treeViewState[TREEVIEWSTATE_showRootLines];
set
{
if (ShowRootLines != value)
{
_treeViewState[TREEVIEWSTATE_showRootLines] = value;
if (IsHandleCreated)
{
UpdateStyles();
}
}
}
}
/// <summary>
/// The Sorted property determines if nodes in the tree view are sorted.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TreeViewSortedDescr))]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Sorted
{
get => _treeViewState[TREEVIEWSTATE_sorted];
set
{
if (Sorted != value)
{
_treeViewState[TREEVIEWSTATE_sorted] = value;
if (Sorted && TreeViewNodeSorter is null && Nodes.Count >= 1)
{
RefreshNodes();
}
}
}
}
/// <summary>
/// The sorting comparer for this TreeView.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TreeViewNodeSorterDescr))]
public IComparer? TreeViewNodeSorter
{
get => _treeViewNodeSorter;
set
{
if (_treeViewNodeSorter != value)
{
_treeViewNodeSorter = value;
if (value is not null)
{
Sort();
}
else
{
Sorted = false;
RefreshNodes();
}
}
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Bindable(false)]
[AllowNull]
public override string Text
{
get => base.Text;
set => base.Text = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? TextChanged
{
add => base.TextChanged += value;
remove => base.TextChanged -= value;
}
/// <summary>
/// The first visible node in the TreeView. Initially
/// the first root node is at the top of the TreeView, but if the
/// contents have been scrolled another node may be at the top.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TreeViewTopNodeDescr))]
public TreeNode? TopNode
{
get
{
if (IsHandleCreated)
{
IntPtr hitem = PInvokeCore.SendMessage(this, PInvoke.TVM_GETNEXTITEM, (WPARAM)PInvoke.TVGN_FIRSTVISIBLE);
return (hitem == IntPtr.Zero ? null : NodeFromHandle(hitem));
}
return _topNode;
}
set
{
if (IsHandleCreated && (value is null || value.TreeView == this))
{
// This class invariant is not quite correct -- if the selected node does not belong to this TreeView,
// selectedNode is not null even though the handle is created. We will call set_SelectedNode
// to inform the handle that the selected node has been added to the TreeView.
Debug.Assert(_topNode is null || _topNode.TreeView != this, "handle is created, but we're still caching selectedNode");
nint hnode = (value is null ? 0 : value.Handle);
PInvokeCore.SendMessage(this, PInvoke.TVM_SELECTITEM, (WPARAM)PInvoke.TVGN_FIRSTVISIBLE, (LPARAM)hnode);
_topNode = null;
}
else
{
_topNode = value;
}
}
}
/// <summary>
/// The count of fully visible nodes in the tree view. This number
/// may be greater than the number of nodes in the control.
/// The control calculates this value by dividing the height of the
/// client window by the height of an item
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TreeViewVisibleCountDescr))]
public int VisibleCount => IsHandleCreated ? (int)PInvokeCore.SendMessage(this, PInvoke.TVM_GETVISIBLECOUNT) : 0;
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewBeforeEditDescr))]
public event NodeLabelEditEventHandler? BeforeLabelEdit
{
add => _onBeforeLabelEdit += value;
remove => _onBeforeLabelEdit -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewAfterEditDescr))]
public event NodeLabelEditEventHandler? AfterLabelEdit
{
add => _onAfterLabelEdit += value;
remove => _onAfterLabelEdit -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewBeforeCheckDescr))]
public event TreeViewCancelEventHandler? BeforeCheck
{
add => _onBeforeCheck += value;
remove => _onBeforeCheck -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewAfterCheckDescr))]
public event TreeViewEventHandler? AfterCheck
{
add => _onAfterCheck += value;
remove => _onAfterCheck -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewBeforeCollapseDescr))]
public event TreeViewCancelEventHandler? BeforeCollapse
{
add => _onBeforeCollapse += value;
remove => _onBeforeCollapse -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewAfterCollapseDescr))]
public event TreeViewEventHandler? AfterCollapse
{
add => _onAfterCollapse += value;
remove => _onAfterCollapse -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewBeforeExpandDescr))]
public event TreeViewCancelEventHandler? BeforeExpand
{
add => _onBeforeExpand += value;
remove => _onBeforeExpand -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewAfterExpandDescr))]
public event TreeViewEventHandler? AfterExpand
{
add => _onAfterExpand += value;
remove => _onAfterExpand -= value;
}
/// <summary>
/// Fires when a TreeView node needs to be drawn.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewDrawNodeEventDescr))]
public event DrawTreeNodeEventHandler? DrawNode
{
add => _onDrawNode += value;
remove => _onDrawNode -= value;
}
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.ListViewItemDragDescr))]
public event ItemDragEventHandler? ItemDrag
{
add => _onItemDrag += value;
remove => _onItemDrag -= value;
}
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.TreeViewNodeMouseHoverDescr))]
public event TreeNodeMouseHoverEventHandler? NodeMouseHover
{
add => _onNodeMouseHover += value;
remove => _onNodeMouseHover -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewBeforeSelectDescr))]
public event TreeViewCancelEventHandler? BeforeSelect
{
add => _onBeforeSelect += value;
remove => _onBeforeSelect -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewAfterSelectDescr))]
public event TreeViewEventHandler? AfterSelect
{
add => _onAfterSelect += value;
remove => _onAfterSelect -= value;
}
/// <summary>
/// TreeView OnPaint.
/// </summary>
/// <hideinheritance/>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event PaintEventHandler? Paint
{
add => base.Paint += value;
remove => base.Paint -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewNodeMouseClickDescr))]
public event TreeNodeMouseClickEventHandler? NodeMouseClick
{
add => _onNodeMouseClick += value;
remove => _onNodeMouseClick -= value;
}
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.TreeViewNodeMouseDoubleClickDescr))]
public event TreeNodeMouseClickEventHandler? NodeMouseDoubleClick
{
add => _onNodeMouseDoubleClick += value;
remove => _onNodeMouseDoubleClick -= value;
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.ControlOnRightToLeftLayoutChangedDescr))]
public event EventHandler? RightToLeftLayoutChanged
{
add => _onRightToLeftLayoutChanged += value;
remove => _onRightToLeftLayoutChanged -= value;
}
/// <summary>
/// Disables redrawing of the tree view. A call to beginUpdate() must be
/// balanced by a following call to endUpdate(). Following a call to
/// beginUpdate(), any redrawing caused by operations performed on the
/// tree view is deferred until the call to endUpdate().
/// </summary>
public void BeginUpdate()
{
BeginUpdateInternal();
}
/// <summary>
/// Collapses all nodes at the root level.
/// </summary>
public void CollapseAll()
{
_root.Collapse();
}
/// <summary>
/// Creates the new instance of AccessibleObject for this TreeView control.
/// </summary>
/// <returns>
/// The AccessibleObject for this TreeView instance.
/// </returns>
protected override AccessibleObject CreateAccessibilityInstance()
=> new TreeViewAccessibleObject(this);
protected override unsafe void CreateHandle()
{
if (!RecreatingHandle)
{
using ThemingScope scope = new(Application.UseVisualStyles);
PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX
{
dwSize = (uint)sizeof(INITCOMMONCONTROLSEX),
dwICC = INITCOMMONCONTROLSEX_ICC.ICC_TREEVIEW_CLASSES
});
}
base.CreateHandle();
}
/// <summary>
/// Resets the imageList to null. We wire this method up to the imageList's
/// Dispose event, so that we don't hang onto an imageList that's gone away.
/// </summary>
private void DetachImageList(object? sender, EventArgs e)
{
ImageList = null;
}
/// <summary>
/// Resets the stateImageList to null. We wire this method up to the stateImageList's
/// Dispose event, so that we don't hang onto an stateImageList that's gone away.
/// </summary>
private void DetachStateImageList(object? sender, EventArgs e)
{
_internalStateImageList = null;
StateImageList = null;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
lock (this)
{
DetachImageListHandlers();
_imageList = null;
DetachStateImageListHandlers();
_stateImageList = null;
}
}
// Dispose unmanaged resources.
UnhookNodes();
KeyboardToolTip.Dispose();
base.Dispose(disposing);
}
/// <summary>
/// Reenables redrawing of the tree view. A call to beginUpdate() must be
/// balanced by a following call to endUpdate(). Following a call to
/// beginUpdate(), any redrawing caused by operations performed on the
/// combo box is deferred until the call to endUpdate().
/// </summary>
public void EndUpdate()
{
EndUpdateInternal();
}
/// <summary>
/// Expands all nodes at the root level.
/// </summary>
public void ExpandAll()
{
_root.ExpandAll();
}
/// <summary>
/// Forces the TreeView to recalculate all its nodes widths so that it updates the
/// scrollbars as appropriate.
/// </summary>
internal void ForceScrollbarUpdate(bool delayed)
{
// ForceScrollbarUpdate call WM_SETREDRAW( FALSE ) followed by WM_SETREDRAW( TRUE )
// So if TreeView.BeginUpdate is called
// ForceScrollbarUpdate effectively causes tree view to ignore BeginUpdate and cause control to update on every change.
// So guard against this scenario by using the new internal method on Control.
if (!IsUpdating())
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)false);
if (delayed)
{
PInvokeCore.PostMessage(this, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)true);
}
else
{
PInvokeCore.SendMessage(this, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)true);
}
}
}
}
/// <summary>
/// Called by ToolTip to poke in that Tooltip into this ComCtl so that the Native ChildToolTip is not exposed.
/// </summary>
internal override void SetToolTip(ToolTip toolTip)
{
if (toolTip is null || !ShowNodeToolTips)
{
return;
}
PInvokeCore.SendMessage(toolTip, PInvoke.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width);
PInvokeCore.SendMessage(this, PInvoke.TVM_SETTOOLTIPS, (WPARAM)toolTip.Handle);
_controlToolTipText = toolTip.GetToolTip(this);
}
/// <summary>
/// Gives the information about which part of the treeNode is at the given point.
/// </summary>
public TreeViewHitTestInfo HitTest(Point pt) => HitTest(pt.X, pt.Y);
/// <summary>
/// Gives the information about which part of the treeNode is at the given x, y.
/// </summary>
public TreeViewHitTestInfo HitTest(int x, int y)
{
TVHITTESTINFO tvhi = new()
{
pt = new Point(x, y)
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhi);
TreeNode? node = hnode == 0 ? null : NodeFromHandle(hnode);
TreeViewHitTestLocations loc = (TreeViewHitTestLocations)tvhi.flags;
return new TreeViewHitTestInfo(node, loc);
}
/// <summary>
/// Defined so that a tree node can use it
/// </summary>
internal bool TreeViewBeforeCheck(TreeNode? node, TreeViewAction actionTaken)
{
TreeViewCancelEventArgs viewCancelEventArgs = new(node, false, actionTaken);
OnBeforeCheck(viewCancelEventArgs);
return viewCancelEventArgs.Cancel;
}
internal void TreeViewAfterCheck(TreeNode node, TreeViewAction actionTaken)
{
OnAfterCheck(new TreeViewEventArgs(node, actionTaken));
}
/// <summary>
/// Returns count of nodes at root, optionally including all subtrees.
/// </summary>
public int GetNodeCount(bool includeSubTrees) => _root.GetNodeCount(includeSubTrees);
/// <summary>
/// Returns the TreeNode at the given location in tree view coordinates.
/// </summary>
public TreeNode? GetNodeAt(Point pt) => GetNodeAt(pt.X, pt.Y);
/// <summary>
/// Returns the TreeNode at the given location in tree view coordinates.
/// </summary>
public TreeNode? GetNodeAt(int x, int y)
{
TVHITTESTINFO tvhi = new()
{
pt = new Point(x, y)
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhi);
return (hnode == 0 ? null : NodeFromHandle(hnode));
}
private void ImageListRecreateHandle(object? sender, EventArgs e)
{
if (IsHandleCreated)
{
IntPtr handle = (ImageList is null) ? IntPtr.Zero : ImageList.Handle;
PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, 0, handle);
}
}
private static void UpdateImagesRecursive(TreeNode node)
{
node.UpdateImage();
// Iterate only through the Nodes collection rather than the
// array since an item might have been removed from the collection, and
// correspondingly "removed" from the array, but still exist in the array
// since the array isn't actually re-dimensioned down to a smaller size.
foreach (TreeNode child in node.Nodes)
{
UpdateImagesRecursive(child);
}
}
private void ImageListChangedHandle(object? sender, EventArgs e)
{
if ((sender is not null) && (sender == _imageList) && IsHandleCreated)
{
BeginUpdate();
foreach (TreeNode node in Nodes)
{
UpdateImagesRecursive(node);
}
EndUpdate();
}
}
private static void NotifyAboutGotFocus(TreeNode? treeNode)
{
if (treeNode is not null)
{
KeyboardToolTipStateMachine.Instance.NotifyAboutGotFocus(treeNode);
}
}
private static void NotifyAboutLostFocus(TreeNode? treeNode)
{
if (treeNode is not null)
{
KeyboardToolTipStateMachine.Instance.NotifyAboutLostFocus(treeNode);
}
}
private void StateImageListRecreateHandle(object? sender, EventArgs e)
{
if (IsHandleCreated)
{
IntPtr handle = IntPtr.Zero;
if (_internalStateImageList is not null)
{
handle = _internalStateImageList.Handle;
}
SetStateImageList(handle);
}
}
private void StateImageListChangedHandle(object? sender, EventArgs e)
{
if (sender is null || sender != _stateImageList || !IsHandleCreated)
{
return;
}
// Since the native treeView requires the state imageList to be 1-indexed we need to
// re add the images if the original collection had changed.
if (_stateImageList is null || _stateImageList.Images.Count <= 0)
{
UpdateCheckedState(_root, true);
return;
}
Image[] images = new Image[_stateImageList.Images.Count + 1];
images[0] = _stateImageList.Images[0];
for (int i = 1; i <= _stateImageList.Images.Count; i++)
{
images[i] = _stateImageList.Images[i - 1];
}
if (_internalStateImageList is not null)
{
_internalStateImageList.Images.Clear();
_internalStateImageList.Images.AddRange(images);
}
else
{
_internalStateImageList = new ImageList();
_internalStateImageList.Images.AddRange(images);
}
Debug.Assert(_internalStateImageList is not null, "Why are changing images when the Imagelist is null?");
if (_internalStateImageList is not null)
{
if (StateImageSize is not null)
{
_internalStateImageList.ImageSize = (Size)StateImageSize;
}
SetStateImageList(_internalStateImageList.Handle);
}
}
/// <summary>
/// Overridden to handle RETURN key.
/// </summary>
protected override bool IsInputKey(Keys keyData)
{
// If in edit mode, treat Return as an input key, so the form doesn't grab it
// and treat it as clicking the Form.AcceptButton. Similarly for Escape
// and Form.CancelButton.
if (_editNode is not null && (keyData & Keys.Alt) == 0)
{
switch (keyData & Keys.KeyCode)
{
case Keys.Return:
case Keys.Escape:
case Keys.PageUp:
case Keys.PageDown:
case Keys.Home:
case Keys.End:
return true;
}
}
return base.IsInputKey(keyData);
}
/// <summary>
/// Note this can be null - particularly if any windows messages get generated during
/// the insertion of a tree node (TVM_INSERTITEM)
/// </summary>
internal TreeNode? NodeFromHandle(IntPtr handle)
{
_nodesByHandle.TryGetValue(handle, out TreeNode? treeNode);
return treeNode;
}
/// <summary>
/// Raises the DrawNode event.
/// </summary>
protected virtual void OnDrawNode(DrawTreeNodeEventArgs e) =>
_onDrawNode?.Invoke(this, e);
protected override void OnHandleCreated(EventArgs e)
{
if (!IsHandleCreated)
{
base.OnHandleCreated(e);
return;
}
TreeNode? savedSelectedNode = _selectedNode;
_selectedNode = null;
base.OnHandleCreated(e);
// The TreeView extended styles are independent of the window extended styles.
UpdateTreeViewExtendedStyles();
int version = (int)PInvokeCore.SendMessage(this, PInvoke.CCM_GETVERSION);
if (version < 5)
{
PInvokeCore.SendMessage(this, PInvoke.CCM_SETVERSION, 5);
}
// Workaround for problem in TreeView where it doesn't recognize the TVS_CHECKBOXES
// style if it is set before the window is created. To get around the problem,
// we set it here after the window is created, and we make sure we don't set it
// in getCreateParams so that this will actually change the value of the bit.
// This seems to make the TreeView happy.
if (CheckBoxes)
{
int style = (int)PInvokeCore.GetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
style |= (int)PInvoke.TVS_CHECKBOXES;
PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
}
if (ShowNodeToolTips && !DesignMode)
{
int style = (int)PInvokeCore.GetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
style |= (int)PInvoke.TVS_INFOTIP;
PInvokeCore.SetWindowLong(this, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
}
Color c = BackColor;
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (c != SystemColors.Window || Application.IsDarkModeEnabled)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETBKCOLOR, 0, c.ToWin32());
}
c = ForeColor;
if (c != SystemColors.WindowText || Application.IsDarkModeEnabled)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETTEXTCOLOR, 0, c.ToWin32());
}
#pragma warning restore WFO5001
// Put the LineColor into the native control only if set.
if (_lineColor != Color.Empty)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETLINECOLOR, 0, _lineColor.ToWin32());
}
if (_imageList is not null)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, 0, _imageList.Handle);
}
if (_stateImageList is not null)
{
UpdateNativeStateImageList();
}
if (_indent != -1)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETINDENT, (WPARAM)_indent);
}
if (_itemHeight != -1)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETITEMHEIGHT, (WPARAM)ItemHeight);
}
// Essentially we are setting the width to be infinite so that the
// TreeView never thinks it needs a scrollbar when the first node is created
// during the first handle creation.
//
// This is set back to the oldSize after the Realize method.
try
{
_treeViewState[TREEVIEWSTATE_stopResizeWindowMsgs] = true;
int oldSize = Width;
SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOZORDER
| SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE
| SET_WINDOW_POS_FLAGS.SWP_NOMOVE;
PInvoke.SetWindowPos(
this,
HWND.HWND_TOP,
Left,
Top,
int.MaxValue,
Height,
flags);
_root.Realize(insertFirst: false);
if (oldSize != 0)
{
PInvoke.SetWindowPos(
this,
HWND.HWND_TOP,
Left,
Top,
oldSize,
Height,
flags);
}
}
finally
{
_treeViewState[TREEVIEWSTATE_stopResizeWindowMsgs] = false;
}
SelectedNode = savedSelectedNode;
}
// Replace the native control's ImageList with our current stateImageList
// set the value of internalStateImageList to the new list
private void UpdateNativeStateImageList()
{
if (_stateImageList is null || _stateImageList.Images.Count <= 0)
{
return;
}
ImageList newImageList = new();
if (StateImageSize is not null)
{
newImageList.ImageSize = (Size)StateImageSize;
}
Image[] images = new Image[_stateImageList.Images.Count + 1];
images[0] = _stateImageList.Images[0];
for (int i = 1; i <= _stateImageList.Images.Count; i++)
{
images[i] = _stateImageList.Images[i - 1];
}
newImageList.Images.AddRange(images);
PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, (WPARAM)PInvoke.TVSIL_STATE, (LPARAM)newImageList.Handle);
_internalStateImageList?.Dispose();
_internalStateImageList = newImageList;
}
private void SetStateImageList(IntPtr handle)
{
// In certain cases (TREEVIEWSTATE_checkBoxes) e.g., the Native TreeView leaks the imageList
// even if set by us. To prevent any leaks, we always destroy what was there after setting a new list.
IntPtr handleOld = PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, (WPARAM)PInvoke.TVSIL_STATE, (LPARAM)handle);
if ((handleOld != IntPtr.Zero) && (handleOld != handle))
{
PInvoke.ImageList.Destroy(new HandleRef<HIMAGELIST>(this, (HIMAGELIST)handleOld));
}
}
// Destroying the tree-view control does not destroy the native state image list.
// We must destroy it explicitly.
private void DestroyNativeStateImageList(bool reset)
{
IntPtr handle = PInvokeCore.SendMessage(this, PInvoke.TVM_GETIMAGELIST, (WPARAM)PInvoke.TVSIL_STATE);
if (handle != IntPtr.Zero)
{
PInvoke.ImageList.Destroy(new HandleRef<HIMAGELIST>(this, (HIMAGELIST)handle));
if (reset)
{
PInvokeCore.SendMessage(this, PInvoke.TVM_SETIMAGELIST, (WPARAM)PInvoke.TVSIL_STATE);
}
}
}
protected override void OnHandleDestroyed(EventArgs e)
{
_selectedNode = SelectedNode;
// Unfortunately, to avoid the native tree view leaking it's State Image List, we need to
// destroy it ourselves here.
DestroyNativeStateImageList(true);
// for the case when we are NOT being disposed, we'll be recreating the internal state imageList
// in OnHandleCreate, so it is ok to completely Dispose here
if (_internalStateImageList is not null)
{
_internalStateImageList.Dispose();
_internalStateImageList = null;
}
base.OnHandleDestroyed(e);
}
/// <summary>
/// We keep track of if we've hovered already so we don't fire multiple hover events
/// </summary>
protected override void OnMouseLeave(EventArgs e)
{
_hoveredAlready = false;
base.OnMouseLeave(e);
}
/// <summary>
/// In order for the MouseHover event to fire for each item in a TreeView,
/// the node the mouse is hovering over is found. Each time a new node is hovered
/// over a new event is raised.
/// </summary>
protected override void OnMouseHover(EventArgs e)
{
// Hover events need to be caught for each node within the TreeView so
// the appropriate NodeHovered event can be raised.
TVHITTESTINFO tvhip = new()
{
pt = PointToClient(Cursor.Position)
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhip);
if (hnode != 0 && ((tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEM) != 0))
{
TreeNode? tn = NodeFromHandle(hnode);
if (tn != _prevHoveredNode && tn is not null)
{
OnNodeMouseHover(new TreeNodeMouseHoverEventArgs(tn));
_prevHoveredNode = tn;
NotifyAboutLostFocus(SelectedNode);
}
}
if (!_hoveredAlready)
{
base.OnMouseHover(e);
_hoveredAlready = true;
}
ResetMouseEventArgs();
}
/// <summary>
/// Raises the beforeLabelEdit event.
/// </summary>
protected virtual void OnBeforeLabelEdit(NodeLabelEditEventArgs e) =>
_onBeforeLabelEdit?.Invoke(this, e);
/// <summary>
/// Raises the afterLabelEdit event.
/// </summary>
protected virtual void OnAfterLabelEdit(NodeLabelEditEventArgs e)
{
_onAfterLabelEdit?.Invoke(this, e);
// Raise an event to highlight & announce the edited node
// if editing hasn't been canceled.
if (IsAccessibilityObjectCreated && !e.CancelEdit && e.Node is not null)
{
e.Node.AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
}
/// <summary>
/// Raises the beforeCheck event.
/// </summary>
protected virtual void OnBeforeCheck(TreeViewCancelEventArgs e) =>
_onBeforeCheck?.Invoke(this, e);
/// <summary>
/// Raises the afterCheck event.
/// </summary>
protected virtual void OnAfterCheck(TreeViewEventArgs e)
{
_onAfterCheck?.Invoke(this, e);
// Raise an event to announce a toggle state change.
if (IsAccessibilityObjectCreated && e.Node is not null)
{
TreeNodeAccessibleObject? nodeAccessibleObject = e.Node.AccessibilityObject;
if (nodeAccessibleObject is null)
{
return;
}
ToggleState newState = nodeAccessibleObject.ToggleState;
ToggleState oldState = newState == ToggleState.ToggleState_On
? ToggleState.ToggleState_Off
: ToggleState.ToggleState_On;
nodeAccessibleObject.RaiseAutomationPropertyChangedEvent(
UIA_PROPERTY_ID.UIA_ToggleToggleStatePropertyId,
oldValue: (VARIANT)(int)oldState,
newValue: (VARIANT)(int)newState);
}
}
/// <summary>
/// Raises the beforeCollapse event.
/// </summary>
protected internal virtual void OnBeforeCollapse(TreeViewCancelEventArgs e) =>
_onBeforeCollapse?.Invoke(this, e);
/// <summary>
/// Raises the afterCollapse event.
/// </summary>
protected internal virtual void OnAfterCollapse(TreeViewEventArgs e)
{
_onAfterCollapse?.Invoke(this, e);
// Raise an event to announce the expand-collapse state change.
if (IsAccessibilityObjectCreated && e.Node is not null)
{
e.Node.AccessibilityObject?.RaiseAutomationPropertyChangedEvent(
UIA_PROPERTY_ID.UIA_ExpandCollapseExpandCollapseStatePropertyId,
oldValue: (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Expanded,
newValue: (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Collapsed);
}
}
/// <summary>
/// Raises the beforeExpand event.
/// </summary>
protected virtual void OnBeforeExpand(TreeViewCancelEventArgs e) =>
_onBeforeExpand?.Invoke(this, e);
/// <summary>
/// Raises the afterExpand event.
/// </summary>
protected virtual void OnAfterExpand(TreeViewEventArgs e)
{
_onAfterExpand?.Invoke(this, e);
// Raise an event to announce the expand-collapse state change.
if (IsAccessibilityObjectCreated && e.Node is not null)
{
e.Node.AccessibilityObject?.RaiseAutomationPropertyChangedEvent(
UIA_PROPERTY_ID.UIA_ExpandCollapseExpandCollapseStatePropertyId,
oldValue: (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Collapsed,
newValue: (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Expanded);
}
}
/// <summary>
/// Raises the ItemDrag event.
/// </summary>
protected virtual void OnItemDrag(ItemDragEventArgs e) =>
_onItemDrag?.Invoke(this, e);
/// <summary>
/// Raises the NodeMouseHover event.
/// </summary>
protected virtual void OnNodeMouseHover(TreeNodeMouseHoverEventArgs e) =>
_onNodeMouseHover?.Invoke(this, e);
/// <summary>
/// Raises the beforeSelect event.
/// </summary>
protected virtual void OnBeforeSelect(TreeViewCancelEventArgs e) =>
_onBeforeSelect?.Invoke(this, e);
/// <summary>
/// Raises the afterSelect event.
/// </summary>
protected virtual void OnAfterSelect(TreeViewEventArgs e)
{
_onAfterSelect?.Invoke(this, e);
// Raise an event to highlight & announce the selected node.
if (IsAccessibilityObjectCreated && e.Node is not null)
{
TreeNodeAccessibleObject? nodeAccessibleObject = e.Node.AccessibilityObject;
if (nodeAccessibleObject is null)
{
return;
}
nodeAccessibleObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
nodeAccessibleObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_SelectionItem_ElementSelectedEventId);
// Raise to say "Selected" after announcing the node.
nodeAccessibleObject.RaiseAutomationPropertyChangedEvent(
UIA_PROPERTY_ID.UIA_SelectionItemIsSelectedPropertyId,
oldValue: (VARIANT)!nodeAccessibleObject.IsItemSelected,
newValue: (VARIANT)nodeAccessibleObject.IsItemSelected);
}
}
/// <summary>
/// Raises the onNodeMouseClick event.
/// </summary>
protected virtual void OnNodeMouseClick(TreeNodeMouseClickEventArgs e) =>
_onNodeMouseClick?.Invoke(this, e);
/// <summary>
/// Raises the onNodeMouseDoubleClick event.
/// </summary>
protected virtual void OnNodeMouseDoubleClick(TreeNodeMouseClickEventArgs e) =>
_onNodeMouseDoubleClick?.Invoke(this, e);
/// <summary>
/// Handles the OnBeforeCheck / OnAfterCheck for keyboard clicks
/// </summary>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Handled)
{
return;
}
// if it's a space, send the check notifications and toggle the checkbox if we're not
// cancelled.
if (CheckBoxes && (e.KeyData & Keys.KeyCode) == Keys.Space)
{
TreeNode? node = SelectedNode;
if (node is not null)
{
bool eventReturn = TreeViewBeforeCheck(node, TreeViewAction.ByKeyboard);
if (!eventReturn)
{
node.CheckedInternal = !node.CheckedInternal;
TreeViewAfterCheck(node, TreeViewAction.ByKeyboard);
}
e.Handled = true;
return;
}
}
}
/// <summary>
/// Handles the OnBeforeCheck / OnAfterCheck for keyboard clicks
/// </summary>
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
if (e.Handled)
{
return;
}
// eat the space key
if ((e.KeyData & Keys.KeyCode) == Keys.Space)
{
e.Handled = true;
return;
}
}
/// <summary>
/// Handles the OnBeforeCheck / OnAfterCheck for keyboard clicks
/// </summary>
protected override void OnKeyPress(KeyPressEventArgs e)
{
base.OnKeyPress(e);
if (e.Handled)
{
return;
}
// eat the space key
if (e.KeyChar == ' ')
{
e.Handled = true;
}
}
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnRightToLeftLayoutChanged(EventArgs e)
{
if (GetAnyDisposingInHierarchy())
{
return;
}
if (RightToLeft == RightToLeft.Yes)
{
RecreateHandle();
}
_onRightToLeftLayoutChanged?.Invoke(this, e);
}
// Refresh the nodes by clearing the tree and adding the nodes back again
//
private void RefreshNodes()
{
TreeNode[] nodes = new TreeNode[Nodes.Count];
Nodes.CopyTo(nodes, 0);
Nodes.Clear();
Nodes.AddRange(nodes);
}
/// <summary>
/// This resets the indentation to the system default.
/// </summary>
private void ResetIndent()
{
_indent = -1;
// is this overkill?
RecreateHandle();
}
/// <summary>
/// This resets the item height to the system default.
/// </summary>
private void ResetItemHeight()
{
_itemHeight = -1;
RecreateHandle();
}
/// <summary>
/// Retrieves true if the indent should be persisted in code gen.
/// </summary>
private bool ShouldSerializeIndent() => (_indent != -1);
/// <summary>
/// Retrieves true if the itemHeight should be persisted in code gen.
/// </summary>
private bool ShouldSerializeItemHeight() => (_itemHeight != -1);
private bool ShouldSerializeSelectedImageIndex()
{
if (_imageList is not null)
{
return SelectedImageIndex != 0;
}
return SelectedImageIndex != ImageList.Indexer.DefaultIndex;
}
private bool ShouldSerializeImageIndex()
{
if (_imageList is not null)
{
return ImageIndex != 0;
}
return ImageIndex != ImageList.Indexer.DefaultIndex;
}
/// <summary>
/// Updated the sorted order
/// </summary>
public void Sort()
{
Sorted = true;
RefreshNodes();
}
internal override bool SupportsUiaProviders => true;
/// <summary>
/// Returns a string representation for this control.
/// </summary>
public override string ToString()
{
string s = base.ToString();
if (Nodes is not null)
{
s += $", Nodes.Count: {Nodes.Count}";
if (Nodes.Count > 0)
{
s += $", Nodes[0]: {Nodes[0]}";
}
}
return s;
}
private unsafe void TvnBeginDrag(MouseButtons buttons, NMTREEVIEWW* nmtv)
{
TVITEMW item = nmtv->itemNew;
// Check for invalid node handle
if (item.hItem == IntPtr.Zero)
{
return;
}
TreeNode? node = NodeFromHandle(item.hItem);
OnItemDrag(new ItemDragEventArgs(buttons, node));
}
private unsafe nint TvnExpanding(NMTREEVIEWW* nmtv)
{
TVITEMW item = nmtv->itemNew;
// Check for invalid node handle
if (item.hItem == IntPtr.Zero)
{
return 0;
}
TreeViewCancelEventArgs? e = null;
if ((item.state & TREE_VIEW_ITEM_STATE_FLAGS.TVIS_EXPANDED) == 0)
{
e = new TreeViewCancelEventArgs(NodeFromHandle(item.hItem), false, TreeViewAction.Expand);
OnBeforeExpand(e);
}
else
{
e = new TreeViewCancelEventArgs(NodeFromHandle(item.hItem), false, TreeViewAction.Collapse);
OnBeforeCollapse(e);
}
return e.Cancel ? 1 : 0;
}
private unsafe void TvnExpanded(NMTREEVIEWW* nmtv)
{
TVITEMW item = nmtv->itemNew;
// Check for invalid node handle
if (item.hItem == IntPtr.Zero)
{
return;
}
TreeViewEventArgs e;
TreeNode? node = NodeFromHandle(item.hItem);
// Note that IsExpanded is invalid for the moment, so we use item item.state to branch.
if ((item.state & TREE_VIEW_ITEM_STATE_FLAGS.TVIS_EXPANDED) == 0)
{
e = new TreeViewEventArgs(node, TreeViewAction.Collapse);
OnAfterCollapse(e);
}
else
{
e = new TreeViewEventArgs(node, TreeViewAction.Expand);
OnAfterExpand(e);
}
}
private unsafe nint TvnSelecting(NMTREEVIEWW* nmtv)
{
if (_treeViewState[TREEVIEWSTATE_ignoreSelects])
{
return 1;
}
// Check for invalid node handle
if (nmtv->itemNew.hItem == IntPtr.Zero)
{
return 0;
}
TreeNode? node = NodeFromHandle(nmtv->itemNew.hItem);
TreeViewAction action = TreeViewAction.Unknown;
switch (nmtv->action)
{
case NM_TREEVIEW_ACTION.TVC_BYKEYBOARD:
action = TreeViewAction.ByKeyboard;
NotifyAboutLostFocus(SelectedNode);
break;
case NM_TREEVIEW_ACTION.TVC_BYMOUSE:
action = TreeViewAction.ByMouse;
break;
}
TreeViewCancelEventArgs e = new(node, false, action);
OnBeforeSelect(e);
return e.Cancel ? 1 : 0;
}
private unsafe void TvnSelected(NMTREEVIEWW* nmtv)
{
// If called from the TreeNodeCollection.Clear() then return.
if (_nodesCollectionClear)
{
return;
}
if (nmtv->itemNew.hItem != IntPtr.Zero)
{
TreeNode? node = NodeFromHandle(nmtv->itemNew.hItem);
TreeViewAction action = TreeViewAction.Unknown;
switch (nmtv->action)
{
case NM_TREEVIEW_ACTION.TVC_BYKEYBOARD:
action = TreeViewAction.ByKeyboard;
NotifyAboutGotFocus(node);
break;
case NM_TREEVIEW_ACTION.TVC_BYMOUSE:
action = TreeViewAction.ByMouse;
break;
}
OnAfterSelect(new TreeViewEventArgs(node, action));
}
// TreeView doesn't properly revert back to the unselected image if the unselected image is blank.
RECT rc = default;
*((IntPtr*)&rc.left) = nmtv->itemOld.hItem;
if (nmtv->itemOld.hItem != IntPtr.Zero)
{
if (PInvokeCore.SendMessage(this, PInvoke.TVM_GETITEMRECT, 1, ref rc) != 0)
{
PInvoke.InvalidateRect(this, &rc, bErase: true);
}
}
}
private LRESULT TvnBeginLabelEdit(NMTVDISPINFOW nmtvdi)
{
// Check for invalid node handle
if (nmtvdi.item.hItem == IntPtr.Zero)
{
return (LRESULT)IntPtr.Zero;
}
Debug.Assert(_labelEdit is null,
"A new label editing shouldn't start before the previous one ended");
if (_labelEdit is not null)
{
_labelEdit.ReleaseHandle();
_labelEdit = null;
}
TreeNode? editingNode = NodeFromHandle(nmtvdi.item.hItem);
NodeLabelEditEventArgs e = new(editingNode);
OnBeforeLabelEdit(e);
if (!e.CancelEdit)
{
_editNode = editingNode;
}
if (!e.CancelEdit)
{
_labelEdit = new TreeViewLabelEditNativeWindow(this);
_labelEdit.AssignHandle(PInvokeCore.SendMessage(this, PInvoke.TVM_GETEDITCONTROL));
}
return (LRESULT)(e.CancelEdit ? 1 : 0);
}
private LRESULT TvnEndLabelEdit(NMTVDISPINFOW nmtvdi)
{
_editNode = null;
// Check for invalid node handle
if (nmtvdi.item.hItem == IntPtr.Zero)
{
return (LRESULT)1;
}
if (_labelEdit is not null)
{
_labelEdit.ReleaseHandle();
_labelEdit = null;
}
TreeNode? node = NodeFromHandle(nmtvdi.item.hItem);
string newText = nmtvdi.item.pszText.ToString();
NodeLabelEditEventArgs e = new(node, newText);
OnAfterLabelEdit(e);
if (newText is not null && !e.CancelEdit && node is not null)
{
node._text = newText;
if (Scrollable)
{
ForceScrollbarUpdate(true);
}
}
return (LRESULT)(e.CancelEdit ? 0 : 1);
}
internal override void UpdateStylesCore()
{
base.UpdateStylesCore();
if (IsHandleCreated && CheckBoxes)
{
if (StateImageList is not null)
{
// Setting the TVS_CHECKBOXES window style also causes the TreeView to display the default checkbox
// images rather than the user specified StateImageList. We send a TVM_SETIMAGELIST to restore the
// user's images.
if (_internalStateImageList is not null)
{
SetStateImageList(_internalStateImageList.Handle);
}
}
}
}
private void UpdateTreeViewExtendedStyles()
{
if (!IsHandleCreated)
{
return;
}
// Only set the TVS_EX_DOUBLEBUFFER style if the DoubleBuffered property setter has been executed.
// This stops the style from being removed for any derived classes that set it using P/Invoke.
if (_treeViewState[TREEVIEWSTATE_doubleBufferedPropertySet])
{
PInvokeCore.SendMessage(this,
PInvoke.TVM_SETEXTENDEDSTYLE,
(WPARAM)(nint)PInvoke.TVS_EX_DOUBLEBUFFER,
(LPARAM)(nint)(DoubleBuffered ? PInvoke.TVS_EX_DOUBLEBUFFER : 0));
}
}
/// <remarks>
/// <para>Setting the PInvoke.TVS_CHECKBOXES style clears the checked state</para>
/// </remarks>
private static void UpdateCheckedState(TreeNode node, bool update)
{
// This looks funny, but CheckedInternal returns the cached isChecked value and the internal
// setter will blindly issue TVM_SETITEM so this gets us back in sync.
if (update)
{
node.CheckedInternal = node.CheckedInternal;
for (int i = node.Nodes.Count - 1; i >= 0; i--)
{
UpdateCheckedState(node.Nodes[i], update);
}
}
else
{
node.CheckedInternal = false;
for (int i = node.Nodes.Count - 1; i >= 0; i--)
{
UpdateCheckedState(node.Nodes[i], update);
}
}
}
private void WmMouseDown(ref Message m, MouseButtons button, int clicks)
{
// Required to put the TreeView in sane-state for painting proper highlighting of selectedNodes.
// If the user shows the ContextMenu bu overriding the WndProc( ), then the treeView
// goes into the weird state where the high-light gets locked to the node on which the ContextMenu was shown.
// So we need to get the native TREEVIEW out of this weird state.
PInvokeCore.SendMessage(this, PInvoke.TVM_SELECTITEM, (WPARAM)PInvoke.TVGN_DROPHILITE);
// Windows TreeView pushes its own message loop in WM_xBUTTONDOWN, so fire the
// event before calling defWndProc or else it won't get fired until the button
// comes back up.
OnMouseDown(new MouseEventArgs(button, clicks, PARAM.ToPoint(m.LParamInternal)));
// If Validation is cancelled don't fire any events through the Windows TreeView's message loop.
if (!ValidationCancelled)
{
DefWndProc(ref m);
}
}
/// <summary>
/// Performs custom draw handling
/// </summary>
private unsafe void CustomDraw(ref Message m)
{
NMTVCUSTOMDRAW* nmtvcd = (NMTVCUSTOMDRAW*)(nint)m.LParamInternal;
// Find out which stage we're drawing
switch (nmtvcd->nmcd.dwDrawStage)
{
// Do we want OwnerDraw for this paint cycle?
case NMCUSTOMDRAW_DRAW_STAGE.CDDS_PREPAINT:
m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NOTIFYITEMDRAW; // yes, we do...
return;
// We've got opt-in on owner draw for items - so handle each one.
case NMCUSTOMDRAW_DRAW_STAGE.CDDS_ITEMPREPAINT:
// get the node
Debug.Assert(nmtvcd->nmcd.dwItemSpec != 0, "Invalid node handle in ITEMPREPAINT");
TreeNode? node = NodeFromHandle((nint)nmtvcd->nmcd.dwItemSpec);
if (node is null)
{
// this can happen if we are presently inserting the node - it hasn't yet
// been added to the handle table
m.ResultInternal = (LRESULT)(nint)(PInvoke.CDRF_SKIPDEFAULT);
return;
}
NMCUSTOMDRAW_DRAW_STATE_FLAGS state = nmtvcd->nmcd.uItemState;
// The commctrl TreeView allows you to draw the whole row of a node
// or nothing at all. The way we provide OwnerDrawText is by asking it
// to draw everything but the text - to do this, we set text color same
// as background color.
if (_drawMode == TreeViewDrawMode.OwnerDrawText)
{
nmtvcd->clrText = nmtvcd->clrTextBk;
m.ResultInternal = (LRESULT)(nint)(PInvoke.CDRF_NEWFONT | PInvoke.CDRF_NOTIFYPOSTPAINT);
return;
}
else if (_drawMode == TreeViewDrawMode.OwnerDrawAll)
{
DrawTreeNodeEventArgs e;
using (Graphics g = nmtvcd->nmcd.hdc.CreateGraphics())
{
Rectangle bounds = node.RowBounds;
SCROLLINFO si = new()
{
cbSize = (uint)sizeof(SCROLLINFO),
fMask = SCROLLINFO_MASK.SIF_POS
};
if (PInvoke.GetScrollInfo(this, SCROLLBAR_CONSTANTS.SB_HORZ, ref si))
{
// need to get the correct bounds if horizontal scroll bar is shown.
// In this case the bounds.X needs to be negative and width needs to be updated to the increased width (scrolled region).
int value = si.nPos;
if (value > 0)
{
bounds.X -= value;
bounds.Width += value;
}
}
e = new DrawTreeNodeEventArgs(g, node, bounds, (TreeNodeStates)(state));
OnDrawNode(e);
}
if (!e.DrawDefault)
{
m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_SKIPDEFAULT;
return;
}
}
// TreeViewDrawMode.Normal case
OwnerDrawPropertyBag renderinfo = GetItemRenderStyles(node, (int)state);
// TreeView has problems with drawing items at times; it gets confused
// as to which colors apply to which items (see focus rectangle shifting;
// when one item is selected, click and hold on another). This needs to be fixed.
Color riFore = renderinfo.ForeColor;
Color riBack = renderinfo.BackColor;
if (renderinfo is not null && !riFore.IsEmpty)
{
nmtvcd->clrText = ColorTranslator.ToWin32(riFore);
}
if (renderinfo is not null && !riBack.IsEmpty)
{
nmtvcd->clrTextBk = ColorTranslator.ToWin32(riBack);
}
if (renderinfo is not null && renderinfo.Font is not null)
{
// Mess with the DC directly...
PInvokeCore.SelectObject(nmtvcd->nmcd.hdc, renderinfo.FontHandle);
// There is a problem in winctl that clips node fonts if the fontSize
// is larger than the treeView font size. The behavior is much better in comctl 5 and above.
m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NEWFONT;
return;
}
// fall through and do the default drawing work
goto default;
case NMCUSTOMDRAW_DRAW_STAGE.CDDS_ITEMPOSTPAINT:
// User draws only the text in OwnerDrawText mode, as explained in comments above
if (_drawMode == TreeViewDrawMode.OwnerDrawText)
{
Debug.Assert(nmtvcd->nmcd.dwItemSpec != 0, "Invalid node handle in ITEMPOSTPAINT");
// Get the node
node = NodeFromHandle((nint)nmtvcd->nmcd.dwItemSpec);
if (node is null)
{
// this can happen if we are presently inserting the node - it hasn't yet
// been added to the handle table
return;
}
using (Graphics g = nmtvcd->nmcd.hdc.CreateGraphics())
{
Rectangle bounds = node.Bounds;
Size textSize = TextRenderer.MeasureText(node.Text, node.TreeView!.Font);
Point textLoc = new(bounds.X - 1, bounds.Y); // required to center the text
bounds = new Rectangle(textLoc, new Size(textSize.Width, bounds.Height));
DrawTreeNodeEventArgs e = new(g, node, bounds, (TreeNodeStates)(nmtvcd->nmcd.uItemState));
OnDrawNode(e);
if (e.DrawDefault)
{
// Simulate default text drawing here
TreeNodeStates curState = e.State;
Font font = node.NodeFont ?? node.TreeView.Font;
Color color = (((curState & TreeNodeStates.Selected) == TreeNodeStates.Selected) && node.TreeView.Focused) ? SystemColors.HighlightText : (node.ForeColor != Color.Empty) ? node.ForeColor : node.TreeView.ForeColor;
// Draw the actual node.
if ((curState & TreeNodeStates.Selected) == TreeNodeStates.Selected)
{
g.FillRectangle(SystemBrushes.Highlight, bounds);
ControlPaint.DrawFocusRectangle(g, bounds, color, SystemColors.Highlight);
TextRenderer.DrawText(g, node.Text, font, bounds, color, TextFormatFlags.Default);
}
else
{
using var brush = BackColor.GetCachedSolidBrushScope();
g.FillRectangle(brush, bounds);
TextRenderer.DrawText(g, node.Text, font, bounds, color, TextFormatFlags.Default);
}
}
}
m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_NOTIFYSUBITEMDRAW;
return;
}
goto default;
default:
// just in case we get a spurious message, tell it to do the right thing
m.ResultInternal = (LRESULT)(nint)PInvoke.CDRF_DODEFAULT;
return;
}
}
/// <summary>
/// Generates colors for each item. This can be overridden to provide colors on a per state/per node
/// basis, rather than using the ForeColor/BackColor/NodeFont properties on TreeNode.
/// </summary>
protected OwnerDrawPropertyBag GetItemRenderStyles(TreeNode? node, int state)
{
OwnerDrawPropertyBag retval = new();
if (node is null || node._propBag is null)
{
return retval;
}
// we only change colors if we're displaying things normally
if ((state &
(int)(NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_SELECTED |
NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_GRAYED |
NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_HOT |
NMCUSTOMDRAW_DRAW_STATE_FLAGS.CDIS_DISABLED)) == 0)
{
retval.ForeColor = node._propBag.ForeColor;
retval.BackColor = node._propBag.BackColor;
}
retval.Font = node._propBag.Font;
return retval;
}
internal override unsafe ToolInfoWrapper<Control> GetToolInfoWrapper(TOOLTIP_FLAGS flags, string? caption, ToolTip tooltip)
{
// The "ShowNodeToolTips" flag is required so that when the user hovers over the TreeNode,
// their own tooltip is displayed, not the TreeView tooltip.
// The second condition is necessary for the correct display of the keyboard tooltip,
// since the logic of the external tooltip blocks its display
bool isExternalTooltip = ShowNodeToolTips && tooltip != KeyboardToolTip;
ToolInfoWrapper<Control> wrapper = new(this, flags, isExternalTooltip ? null : caption);
if (isExternalTooltip)
{
wrapper.Info.lpszText = (char*)(-1);
}
return wrapper;
}
private unsafe bool WmShowToolTip(ref Message m)
{
NMHDR* nmhdr = (NMHDR*)(nint)m.LParamInternal;
HWND tooltipHandle = nmhdr->hwndFrom;
TVHITTESTINFO tvhip = new()
{
pt = PointToClient(Cursor.Position)
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhip);
if (hnode != 0 && tvhip.flags.HasFlag(TVHITTESTINFO_FLAGS.TVHT_ONITEM) && NodeFromHandle(hnode) is { } tn && !ShowNodeToolTips)
{
Rectangle bounds = tn.Bounds;
bounds.Location = PointToScreen(bounds.Location);
PInvokeCore.SendMessage(tooltipHandle, PInvoke.TTM_ADJUSTRECT, (WPARAM)(BOOL)true, ref bounds);
PInvoke.SetWindowPos(
tooltipHandle,
HWND.HWND_TOPMOST,
bounds.Left,
bounds.Top,
0,
0,
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER);
return true;
}
return false;
}
private unsafe void WmNeedText(ref Message m)
{
NMTTDISPINFOW* ttt = (NMTTDISPINFOW*)(nint)m.LParamInternal;
string? tipText = _controlToolTipText;
TVHITTESTINFO tvhip = new()
{
pt = PointToClient(Cursor.Position)
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhip);
if (hnode != 0 && ((tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEM) != 0))
{
TreeNode? tn = NodeFromHandle(hnode);
if (ShowNodeToolTips && tn is not null && (!string.IsNullOrEmpty(tn.ToolTipText)))
{
tipText = tn.ToolTipText;
}
else if (tn is not null && tn.Bounds.Right > Bounds.Right)
{
tipText = tn.Text;
}
else
{
tipText = null;
}
}
_toolTipBuffer.SetText(tipText);
ttt->lpszText = (char*)_toolTipBuffer.Buffer;
ttt->hinst = HINSTANCE.Null;
// RightToLeft reading order
if (RightToLeft == RightToLeft.Yes)
{
ttt->uFlags |= TOOLTIP_FLAGS.TTF_RTLREADING;
}
}
private unsafe void WmNotify(ref Message m)
{
NMHDR* nmhdr = (NMHDR*)(nint)m.LParamInternal;
// Custom draw code is handled separately.
if (nmhdr->code == PInvoke.NM_CUSTOMDRAW)
{
CustomDraw(ref m);
}
else
{
NMTREEVIEWW* nmtv = (NMTREEVIEWW*)(nint)m.LParamInternal;
switch (nmtv->hdr.code)
{
case PInvoke.TVN_ITEMEXPANDINGW:
m.ResultInternal = (LRESULT)TvnExpanding(nmtv);
break;
case PInvoke.TVN_ITEMEXPANDEDW:
TvnExpanded(nmtv);
break;
case PInvoke.TVN_SELCHANGINGW:
m.ResultInternal = (LRESULT)TvnSelecting(nmtv);
break;
case PInvoke.TVN_SELCHANGEDW:
TvnSelected(nmtv);
break;
case PInvoke.TVN_BEGINDRAGW:
TvnBeginDrag(MouseButtons.Left, nmtv);
break;
case PInvoke.TVN_BEGINRDRAGW:
TvnBeginDrag(MouseButtons.Right, nmtv);
break;
case PInvoke.TVN_BEGINLABELEDITW:
m.ResultInternal = TvnBeginLabelEdit(*(NMTVDISPINFOW*)(nint)m.LParamInternal);
break;
case PInvoke.TVN_ENDLABELEDITW:
m.ResultInternal = TvnEndLabelEdit(*(NMTVDISPINFOW*)(nint)m.LParamInternal);
break;
case PInvoke.NM_CLICK:
case PInvoke.NM_RCLICK:
MouseButtons button = MouseButtons.Left;
Point pos = PointToClient(Cursor.Position);
TVHITTESTINFO tvhip = new()
{
pt = pos
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhip);
if (nmtv->hdr.code != PInvoke.NM_CLICK || (tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEM) != 0)
{
button = nmtv->hdr.code == PInvoke.NM_CLICK ? MouseButtons.Left : MouseButtons.Right;
}
// The treeView's WndProc doesn't get the WM_LBUTTONUP messages when
// LBUTTONUP happens on TVHT_ONITEM. This is a comctl quirk.
// We work around that by calling OnMouseUp here.
if (nmtv->hdr.code != PInvoke.NM_CLICK
|| (tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEM) != 0 || FullRowSelect)
{
if (hnode != 0 && !ValidationCancelled)
{
OnNodeMouseClick(new TreeNodeMouseClickEventArgs(NodeFromHandle(hnode), button, 1, pos.X, pos.Y));
OnClick(new MouseEventArgs(button, 1, pos.X, pos.Y, 0));
OnMouseClick(new MouseEventArgs(button, 1, pos.X, pos.Y, 0));
}
}
if (nmtv->hdr.code == PInvoke.NM_RCLICK)
{
TreeNode? treeNode = NodeFromHandle(hnode);
if (treeNode is not null && treeNode.ContextMenuStrip is not null)
{
ShowContextMenu(treeNode);
}
else
{
_treeViewState[TREEVIEWSTATE_showTreeViewContextMenu] = true;
PInvokeCore.SendMessage(this, PInvokeCore.WM_CONTEXTMENU, (WPARAM)HWND, (LPARAM)PInvoke.GetMessagePos());
}
m.ResultInternal = (LRESULT)1;
}
if (!_treeViewState[TREEVIEWSTATE_mouseUpFired])
{
if (nmtv->hdr.code != PInvoke.NM_CLICK
|| (tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEM) != 0)
{
// The treeView's WndProc doesn't get the WM_LBUTTONUP messages when
// LBUTTONUP happens on TVHT_ONITEM. This is a comctl quirk.
// We work around that by calling OnMouseUp here.
OnMouseUp(new MouseEventArgs(button, 1, pos.X, pos.Y, 0));
_treeViewState[TREEVIEWSTATE_mouseUpFired] = true;
}
}
break;
}
}
}
protected override void OnGotFocus(EventArgs e)
{
base.OnGotFocus(e);
NotifyAboutGotFocus(SelectedNode);
// Raise an event to highlight & announce the selected node.
if (IsAccessibilityObjectCreated)
{
SelectedNode?.AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
}
protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
NotifyAboutLostFocus(SelectedNode);
}
/// <summary>
/// Shows the context menu for the <see cref="TreeNode"/>.
/// </summary>
private void ShowContextMenu(TreeNode treeNode)
{
if (treeNode.ContextMenuStrip is not null)
{
ContextMenuStrip menu = treeNode.ContextMenuStrip;
// Need to send TVM_SELECTITEM to highlight the node while the contextMenuStrip is being shown.
PInvokeCore.PostMessage(this, PInvoke.TVM_SELECTITEM, (WPARAM)PInvoke.TVGN_DROPHILITE, (LPARAM)treeNode.Handle);
menu.ShowInternal(this, PointToClient(MousePosition), isKeyboardActivated: false);
menu.Closing += ContextMenuStripClosing;
}
}
// Need to send TVM_SELECTITEM to reset the node-highlighting while the contextMenuStrip is being closed so that the
// treeView reselects the SelectedNode.
private void ContextMenuStripClosing(object? sender, ToolStripDropDownClosingEventArgs e)
{
ContextMenuStrip strip = (ContextMenuStrip)sender!;
// Unhook the Event.
strip.Closing -= ContextMenuStripClosing;
PInvokeCore.SendMessage(this, PInvoke.TVM_SELECTITEM, (WPARAM)PInvoke.TVGN_DROPHILITE);
}
private void UnhookNodes()
{
foreach (TreeNode rootNode in Nodes)
{
foreach (TreeNode node in rootNode.GetSelfAndChildNodes())
{
KeyboardToolTipStateMachine.Instance.Unhook(node, KeyboardToolTip);
}
}
}
private void WmPrint(ref Message m)
{
base.WndProc(ref m);
if (((nint)m.LParamInternal & PInvoke.PRF_NONCLIENT) != 0
&& Application.RenderWithVisualStyles && BorderStyle == BorderStyle.Fixed3D)
{
using Graphics g = Graphics.FromHdc((HDC)m.WParamInternal);
Rectangle rect = new(0, 0, Size.Width - 1, Size.Height - 1);
using var pen = VisualStyleInformation.TextControlBorder.GetCachedPenScope();
g.DrawRectangle(pen, rect);
rect.Inflate(-1, -1);
g.DrawRectangle(SystemPens.Window, rect);
}
}
protected override unsafe void WndProc(ref Message m)
{
switch (m.MsgInternal)
{
case PInvokeCore.WM_WINDOWPOSCHANGING:
case PInvokeCore.WM_NCCALCSIZE:
case PInvokeCore.WM_WINDOWPOSCHANGED:
case PInvokeCore.WM_SIZE:
// While we are changing size of treeView to avoid the scrollbar; don't respond to the window-sizing messages.
if (_treeViewState[TREEVIEWSTATE_stopResizeWindowMsgs])
{
DefWndProc(ref m);
}
else
{
base.WndProc(ref m);
}
break;
case PInvokeCore.WM_HSCROLL:
base.WndProc(ref m);
if (DrawMode == TreeViewDrawMode.OwnerDrawAll)
{
Invalidate();
}
break;
case PInvokeCore.WM_PRINT:
WmPrint(ref m);
break;
case PInvoke.TVM_SETITEMW:
base.WndProc(ref m);
if (CheckBoxes)
{
TVITEMW* item = (TVITEMW*)(nint)m.LParamInternal;
// Check for invalid node handle
if (item->hItem != IntPtr.Zero)
{
TVITEMW item1 = new()
{
mask = TVITEM_MASK.TVIF_HANDLE | TVITEM_MASK.TVIF_STATE,
hItem = item->hItem,
stateMask = TREE_VIEW_ITEM_STATE_FLAGS.TVIS_STATEIMAGEMASK
};
PInvokeCore.SendMessage(this, PInvoke.TVM_GETITEMW, 0, ref item1);
TreeNode node = NodeFromHandle(item->hItem)!;
node.CheckedStateInternal = (((int)item1.state >> SHIFTVAL) > 1);
}
}
break;
case PInvokeCore.WM_NOTIFY:
NMHDR* nmhdr = (NMHDR*)(nint)m.LParamInternal;
switch (nmhdr->code)
{
case PInvoke.TTN_GETDISPINFOW:
// Setting the max width has the added benefit of enabling multiline tool tips
PInvokeCore.SendMessage(nmhdr->hwndFrom, PInvoke.TTM_SETMAXTIPWIDTH, 0, SystemInformation.MaxWindowTrackSize.Width);
WmNeedText(ref m);
m.ResultInternal = (LRESULT)1;
return;
case PInvoke.TTN_SHOW:
if (WmShowToolTip(ref m))
{
m.ResultInternal = (LRESULT)1;
return;
}
else
{
base.WndProc(ref m);
break;
}
default:
base.WndProc(ref m);
break;
}
break;
case MessageId.WM_REFLECT_NOTIFY:
WmNotify(ref m);
break;
case PInvokeCore.WM_LBUTTONDBLCLK:
WmMouseDown(ref m, MouseButtons.Left, 2);
// Just maintain state and fire double click in final mouseUp.
_treeViewState[TREEVIEWSTATE_doubleclickFired] = true;
// Fire mouse up in the Wndproc.
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
// Make sure we get the mouse up if it happens outside the control.
Capture = true;
break;
case PInvokeCore.WM_LBUTTONDOWN:
try
{
_treeViewState[TREEVIEWSTATE_ignoreSelects] = true;
Focus();
}
finally
{
_treeViewState[TREEVIEWSTATE_ignoreSelects] = false;
}
// Always reset the MouseUpFired.
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
TVHITTESTINFO tvhip = new()
{
pt = PARAM.ToPoint(m.LParamInternal)
};
_mouseDownNode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhip);
// This gets around the TreeView behavior of temporarily moving the selection
// highlight to a node when the user clicks on its checkbox.
if ((tvhip.flags & TVHITTESTINFO_FLAGS.TVHT_ONITEMSTATEICON) != 0)
{
// We do not pass the Message to the Control so fire MouseDown.
OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, PARAM.ToPoint(m.LParamInternal)));
if (!ValidationCancelled && CheckBoxes)
{
TreeNode? node = NodeFromHandle(_mouseDownNode);
bool eventReturn = TreeViewBeforeCheck(node, TreeViewAction.ByMouse);
if (!eventReturn && node is not null)
{
node.CheckedInternal = !node.CheckedInternal;
TreeViewAfterCheck(node, TreeViewAction.ByMouse);
}
}
m.ResultInternal = (LRESULT)0;
}
else
{
WmMouseDown(ref m, MouseButtons.Left, 1);
}
_downButton = MouseButtons.Left;
break;
case PInvokeCore.WM_LBUTTONUP:
case PInvokeCore.WM_RBUTTONUP:
Point point = PARAM.ToPoint(m.LParamInternal);
TVHITTESTINFO tvhi = new()
{
pt = point
};
nint hnode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhi);
// Important for CheckBoxes. Click needs to be fired.
if (hnode != 0)
{
if (!ValidationCancelled && !_treeViewState[TREEVIEWSTATE_doubleclickFired] & !_treeViewState[TREEVIEWSTATE_mouseUpFired])
{
// If the hit-tested node here is the same as the node we hit-tested
// on mouse down then we will fire our OnNodeMouseClick event.
if (hnode == _mouseDownNode)
{
OnNodeMouseClick(new TreeNodeMouseClickEventArgs(NodeFromHandle(hnode), _downButton, 1, point.X, point.Y));
}
OnClick(new MouseEventArgs(_downButton, 1, point));
OnMouseClick(new MouseEventArgs(_downButton, 1, point));
}
if (_treeViewState[TREEVIEWSTATE_doubleclickFired])
{
_treeViewState[TREEVIEWSTATE_doubleclickFired] = false;
if (!ValidationCancelled)
{
OnNodeMouseDoubleClick(new TreeNodeMouseClickEventArgs(NodeFromHandle(hnode), _downButton, 2, point.X, point.Y));
OnDoubleClick(new MouseEventArgs(_downButton, 2, point));
OnMouseDoubleClick(new MouseEventArgs(_downButton, 2, point));
}
}
}
if (!_treeViewState[TREEVIEWSTATE_mouseUpFired])
{
OnMouseUp(new MouseEventArgs(_downButton, 1, point));
}
_treeViewState[TREEVIEWSTATE_doubleclickFired] = false;
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
Capture = false;
// Always clear our hit-tested node we cached on mouse down
_mouseDownNode = IntPtr.Zero;
break;
case PInvokeCore.WM_MBUTTONDBLCLK:
// Fire mouse up in the Wndproc.
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
WmMouseDown(ref m, MouseButtons.Middle, 2);
break;
case PInvokeCore.WM_MBUTTONDOWN:
// Always reset MouseUpFired.
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
WmMouseDown(ref m, MouseButtons.Middle, 1);
_downButton = MouseButtons.Middle;
break;
case PInvokeCore.WM_MOUSELEAVE:
// if the mouse leaves and then reenters the TreeView
// NodeHovered events should be raised.
_prevHoveredNode = null;
base.WndProc(ref m);
break;
case PInvokeCore.WM_RBUTTONDBLCLK:
WmMouseDown(ref m, MouseButtons.Right, 2);
// Just maintain state and fire double click in the final mouseUp.
_treeViewState[TREEVIEWSTATE_doubleclickFired] = true;
// Fire mouse up in the Wndproc
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
// Make sure we get the mouse up if it happens outside the control.
Capture = true;
break;
case PInvokeCore.WM_RBUTTONDOWN:
// Always Reset the MouseUpFired....
_treeViewState[TREEVIEWSTATE_mouseUpFired] = false;
// Cache the hit-tested node for verification when mouse up is fired
TVHITTESTINFO tvhit = new()
{
pt = PARAM.ToPoint(m.LParamInternal)
};
_mouseDownNode = PInvokeCore.SendMessage(this, PInvoke.TVM_HITTEST, 0, ref tvhit);
WmMouseDown(ref m, MouseButtons.Right, 1);
_downButton = MouseButtons.Right;
break;
case PInvokeCore.WM_SYSCOLORCHANGE:
PInvokeCore.SendMessage(this, PInvoke.TVM_SETINDENT, (WPARAM)Indent);
base.WndProc(ref m);
break;
case PInvokeCore.WM_SETFOCUS:
// If we get focus through the LButtonDown .. we might have done the validation...
// so skip it..
if (_treeViewState[TREEVIEWSTATE_lastControlValidated])
{
_treeViewState[TREEVIEWSTATE_lastControlValidated] = false;
WmImeSetFocus();
DefWndProc(ref m);
InvokeGotFocus(this, EventArgs.Empty);
}
else
{
base.WndProc(ref m);
}
break;
case PInvokeCore.WM_CONTEXTMENU:
if (_treeViewState[TREEVIEWSTATE_showTreeViewContextMenu])
{
_treeViewState[TREEVIEWSTATE_showTreeViewContextMenu] = false;
base.WndProc(ref m);
}
else
{
// this is the Shift + F10 Case....
TreeNode? treeNode = SelectedNode;
if (treeNode is not null && treeNode.ContextMenuStrip is not null)
{
Point client;
client = new Point(treeNode.Bounds.X, treeNode.Bounds.Y + treeNode.Bounds.Height / 2);
// VisualStudio7 # 156, only show the context menu when clicked in the client area
if (ClientRectangle.Contains(client) && treeNode.ContextMenuStrip is not null)
{
bool keyboardActivated = m.LParamInternal == -1;
treeNode.ContextMenuStrip.ShowInternal(this, client, keyboardActivated);
}
}
else
{
// in this case we don't have a selected node. The base
// will ensure we're constrained to the client area.
base.WndProc(ref m);
}
}
break;
case PInvokeCore.WM_GETOBJECT:
// When TreeView is the first control in TabOrder on a form, selection will not be announced when the forms is first opened
// because TreeView's AccessibilityObject has not been created yet. We create this object proactively to support that case.
// You can also refer to this PR. https://github.com/dotnet/winforms/pull/9925
if (m.LParamInternal == PInvoke.UiaRootObjectId && SupportsUiaProviders && !IsAccessibilityObjectCreated && Focused)
{
base.WndProc(ref m);
SelectedNode?.AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
else
{
base.WndProc(ref m);
}
break;
default:
base.WndProc(ref m);
break;
}
}
}
|