Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# variable height for nodes in a TreeView

Despite my google efforts, I could not find a solution to use a default .NET treeview and have variable height for each node in that tree view.

I need is a way to have 2 types of node with different heights.

Ideally, I would also like that one node type can also become bigger as the mouse hover it.

Any clever guy around? :)

like image 267
Jeremy Avatar asked Jan 23 '23 03:01

Jeremy


2 Answers

I realize this is quite old ... but I found something interesting yesterday. This thread contains an answer by Chris Forbes which suggests that it is indeed possible to have items of variable height, if the treeview has the TVS_NONEVENHEIGHT style. I played around with this idea and the github code snippet he linked to, and found that this actually does work but does not provide 100% flexibility (see list of limitations below). I'm also not sure if it would be suitable to change the height of an item on mouse hover.

Why it does what it does is beyond me though, as the window style just seems to enable the user to set an odd item height instead of an even one.

Limitations and caveats:

  • It needs lots of work, as the node must be completely owner drawn. This code example is not fully functional in that respect.
  • You can only set the item height to multiples of the control's ItemHeight property (in fact, you actually set it to the factor you want, 1, 2, 3, ...). I did experiment with setting the control's ItemHeight property to 1 and then set the node height to the pixel height I wanted. It does seem to work, but if you add items at design time, it just produces weird and broken looking results in the designer. I didn't test this thoroughly though.
  • You can't set the height if the node hasn't been added to a TreeNodeCollection, as the TreeNode's handle will not have been created.
  • You can't modify the item height at design time.
  • I don't use pinvoke stuff frequently, so some of those definitions may need some work. For example, the original definition of TVITEMEX has some conditional entries which I didn't know how to replicate.

Here's a 247 line code snippet that demonstrates this, just replace Program.cs of a Windows Forms Application with this code.

It still does need lots of work, as the OwnerDraw code does not yet do anything regarding drawing icons, tree lines, checkboxes and the like, but I thought it was a pretty surprising find, worthy to be posted here.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace TreeTest
{
  static class Program
  {
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      Application.Run(new TreeForm());
    }
  }

  public static class NativeExtensions
  {
    public const int TVS_NONEVENHEIGHT = 0x4000;

    [DllImport("user32")]
    //private static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wp, IntPtr lp);
    private static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wp, ref TVITEMEX lp);

    private const int TVM_GETITEM = 0x1100 + 62;
    private const int TVM_SETITEM = 0x1100 + 63;

    [StructLayout(LayoutKind.Sequential)]
    private struct TVITEMEX
    {
      public uint mask;
      public IntPtr hItem;
      public uint state;
      public uint stateMask;
      public IntPtr pszText;
      public int cchTextMax;
      public int iImage;
      public int iSelectedImage;
      public int cChildren;
      public IntPtr lParam;
      public int iIntegral;
      public uint uStateEx;
      public IntPtr hwnd;
      public int iExpandedImage;
      public int iReserved;
    }

    [Flags]
    private enum Mask : uint
    {
      Text = 1,
      Image = 2,
      Param = 4,
      State = 8,
      Handle = 16,
      SelectedImage = 32,
      Children = 64,
      Integral = 128,
    }

    /// <summary>
    /// Get a node's height. Will throw an error if the Node has not yet been added to a TreeView,
    /// as it's handle will not exist.
    /// </summary>
    /// <param name="tn">TreeNode to work with</param>
    /// <returns>Height in multiples of ItemHeight</returns>
    public static int GetHeight(this TreeNode tn)
    {
      TVITEMEX tvix = new TVITEMEX
      {
        mask = (uint)(Mask.Handle | Mask.Integral),
        hItem = tn.Handle,
        iIntegral = 0
      };
      SendMessage(tn.TreeView.Handle, TVM_GETITEM, IntPtr.Zero, ref tvix);
      return tvix.iIntegral;
    }

    /// <summary>
    /// Set a node's height. Will throw an error if the Node has not yet been added to a TreeView,
    /// as it's handle will not exist.
    /// </summary>
    /// <param name="tn">TreeNode to work with</param>
    /// <param name="height">Height in multiples of ItemHeight</param>
    public static void SetHeight(this TreeNode tn, int height)
    {
      TVITEMEX tvix = new TVITEMEX
      {
        mask = (uint)(Mask.Handle | Mask.Integral),
        hItem = tn.Handle,
        iIntegral = height
      };
      SendMessage(tn.TreeView.Handle, TVM_SETITEM, IntPtr.Zero, ref tvix);
    }
  }

  public class TreeViewTest : TreeView
  {
    public TreeViewTest()
    {
      // Do DoubleBuffered painting
      SetStyle(ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

      // Set value for owner drawing ...
      DrawMode = TreeViewDrawMode.OwnerDrawAll;
    }

    /// <summary>
    /// For TreeNodes to support variable heights, we need to apply the
    /// TVS_NONEVENHEIGHT style to the control.
    /// </summary>
    protected override CreateParams CreateParams
    {
      get
      {
        var cp = base.CreateParams;
        cp.Style |= NativeExtensions.TVS_NONEVENHEIGHT;
        return cp;
      }
    }

    /// <summary>
    /// Do not tempt anyone to change the DrawMode property, be it via code or via
    /// Property grid. It's still possible via code though ...
    /// </summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
      EditorBrowsable(EditorBrowsableState.Never)]
    public new TreeViewDrawMode DrawMode
    {
      get { return base.DrawMode; }
      set { base.DrawMode = value; }
    }

    /// <summary>
    /// OwnerDraw code. Still needs a lot of work, no tree lines, symbols, checkboxes etc. are drawn
    /// yet, just the plain item text and background ...
    /// </summary>
    /// <param name="e"></param>
    protected override void OnDrawNode(DrawTreeNodeEventArgs e)
    {
      e.DrawDefault = false;

      // Draw window colour background
      e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);

      // Draw selected item background
      if (e.Node.IsSelected)
        e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Node.Bounds);

      // Draw item text
      TextRenderer.DrawText(e.Graphics, e.Node.Text, Font, e.Node.Bounds,
        e.Node.IsSelected ? SystemColors.HighlightText : SystemColors.WindowText,
        Color.Transparent, TextFormatFlags.Top | TextFormatFlags.NoClipping);

      // Draw focus rectangle
      if (Focused && e.Node.IsSelected)
        ControlPaint.DrawFocusRectangle(e.Graphics, e.Node.Bounds);

      base.OnDrawNode(e);
    }

    /// <summary>
    /// Without this piece of code, for some reason, drawing of items that get selected/unselected
    /// is deferred until MouseUp is received.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnMouseDown(MouseEventArgs e)
    {
      base.OnMouseDown(e);
      TreeNode clickedNode = GetNodeAt(e.X, e.Y);
      if (clickedNode.Bounds.Contains(e.X, e.Y))
      {
        SelectedNode = clickedNode;
      }
    }
  }

  public class TreeForm : Form
  {
    public TreeForm() { InitializeComponent(); }

    private System.ComponentModel.IContainer components = null;

    protected override void Dispose(bool disposing)
    {
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      base.Dispose(disposing);
    }

    private void InitializeComponent()
    {
      this.treeViewTest1 = new TreeTest.TreeViewTest();
      this.SuspendLayout();
      // 
      // treeViewTest1
      // 
      this.treeViewTest1.Dock = System.Windows.Forms.DockStyle.Fill;
      this.treeViewTest1.Location = new System.Drawing.Point(0, 0);
      this.treeViewTest1.Name = "treeViewTest1";
      this.treeViewTest1.Size = new System.Drawing.Size(284, 262);
      this.treeViewTest1.TabIndex = 0;
      // 
      // Form2
      // 
      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      this.ClientSize = new System.Drawing.Size(284, 262);
      this.Controls.Add(this.treeViewTest1);
      this.Name = "Form2";
      this.Text = "Form2";
      this.ResumeLayout(false);
    }

    private TreeViewTest treeViewTest1;

    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);

      AddNodes(treeViewTest1.Nodes, 0, new Random());
    }

    private void AddNodes(TreeNodeCollection nodes, int depth, Random r)
    {
      if (depth > 2) return;

      for (int i = 0; i < 3; i++)
      {
        int height = r.Next(1, 4);
        TreeNode tn = new TreeNode { Text = $"Node {i + 1} at depth {depth} with height {height}" };
        nodes.Add(tn);
        tn.SetHeight(height);
        AddNodes(tn.Nodes, depth + 1, r);
      }
    }
  }
}
like image 118
takrl Avatar answered Jan 27 '23 10:01

takrl


I didn't find an answer to your question. @Frank is right about this not being possible in WinForms.

However, Microsoft design guidelines offer some alternatives to TreeViews IF your hierarchy is only two levels deep.

From https://learn.microsoft.com/en-us/windows/win32/uxguide/ctrl-tree-views:

You must use a tree view if you need to display a hierarchy of more than two levels (not including the root node).

like image 21
David Avatar answered Jan 27 '23 09:01

David