TreeView 删除某些节点的复选框

28

我想移除Node.Type为5或6的复选框。我使用以下代码:

private void TvOne_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
    int type = (e.Node as Node).typ;
    if (type == 5 || type == 6)
    {
        Color backColor, foreColor;
        if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
        {
            backColor = SystemColors.Highlight;
            foreColor = SystemColors.HighlightText;
        }
        else if ((e.State & TreeNodeStates.Hot) == TreeNodeStates.Hot)
        {
            backColor = SystemColors.HotTrack;
            foreColor = SystemColors.HighlightText;
        }
        else
        {
            backColor = e.Node.BackColor;
            foreColor = e.Node.ForeColor;
        }
        using (SolidBrush brush = new SolidBrush(backColor))
        {
            e.Graphics.FillRectangle(brush, e.Node.Bounds);
        }
        TextRenderer.DrawText(e.Graphics, e.Node.Text, this.TvOne.Font,
            e.Node.Bounds, foreColor, backColor);

        if ((e.State & TreeNodeStates.Focused) == TreeNodeStates.Focused)
        {
            ControlPaint.DrawFocusRectangle(e.Graphics, e.Node.Bounds,
                foreColor, backColor);
        }
        e.DrawDefault = false;
    }
    else
    {
        e.DrawDefault = true;
    }
}
问题是当复选框被移除后,图像和指向根节点的线也一起消失了。 如何移除复选框并保留图像和线?

这是错误的!


可能是重复的问题:我正在寻找关于自绘树形视图的好资源 - Hans Passant
我同意 Hans 的看法;owner-draw 通常非常困难。如果你坚持要做,我在这个问题的回答中找到了一个更完整的 owner-drawn treeview 示例,其中包括节点线。链接 - Cody Gray
4个回答

74
在你现有的代码中,你正在处理所有类型为5或6的节点的绘制。对于其余类型的节点,你只是允许系统以默认方式绘制。这就是为什么它们都有预期的线条,但你正在进行自定义绘制的节点却没有:你忘记了画线!你看,当你说 e.DrawDefault = false; 时,人们会认为你真的是这个意思。没有任何常规的绘制被执行,包括标准线条。
你需要自己绘制这些线条,或者弄清楚如何在完全不使用自定义绘制的情况下解决问题。
从你现有的代码来看,看起来你试图尽可能地模拟系统的本机绘图样式,所以我不确定你通过首先进行自定义绘制到底实现了什么。如果你只是想让类型为5和6的节点不显示复选框(就像线条一样因为你没有绘制它们而被省略了),那么有一种更简单的方法可以做到这一点,而不涉及自定义绘制。
那么,你会问,隐藏单个节点的复选框的更简单方法是什么呢?嗯,事实证明,TreeView控件本身实际上支持此功能,但该功能在.NET Framework中未公开。你需要使用P/Invoke并调用Windows API来实现它。将以下代码添加到你的窗体类中(确保已添加了对System.Runtime.InteropServices命名空间的using声明):
private const int TVIF_STATE = 0x8;
private const int TVIS_STATEIMAGEMASK = 0xF000;
private const int TV_FIRST = 0x1100;
private const int TVM_SETITEM = TV_FIRST + 63;

[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
private struct TVITEM
{
    public int mask;
    public IntPtr hItem;
    public int state;
    public int stateMask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpszText;
    public int cchTextMax;
    public int iImage;
    public int iSelectedImage;
    public int cChildren;
    public IntPtr lParam;
}

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
                                         ref TVITEM lParam);

/// <summary>
/// Hides the checkbox for the specified node on a TreeView control.
/// </summary>
private void HideCheckBox(TreeView tvw, TreeNode node)
{
    TVITEM tvi = new TVITEM();
    tvi.hItem = node.Handle;
    tvi.mask = TVIF_STATE;
    tvi.stateMask = TVIS_STATEIMAGEMASK;
    tvi.state = 0;
    SendMessage(tvw.Handle, TVM_SETITEM, IntPtr.Zero, ref tvi);
}

混乱的部分在顶部是您的P/Invoke声明。 您需要一些常量,描述树形视图项属性的TVITEM结构体以及SendMessage函数。 在底部是实际调用以执行操作的函数(HideCheckBox)。 您只需传入要移除复选框的TreeView控件和特定TreeNode项目即可。

因此,您可以从每个子节点中删除复选框,以获得类似以下内容的结果:

   隐藏子节点的带有复选框的树形视图


1
@Werewolve: 好吧,也许我说错了它更加“简单”。我个人认为它更简单是因为我更熟悉它,而且自定义绘制很难做到完美。基本上,TreeView控件本身支持隐藏单个节点的复选框,但该功能在.NET Framework中没有公开。您需要使用P / Invoke才能获得它。如果您想要,我可以更新我的答案并提供代码。 - Cody Gray
8
请查看此答案以解决这段代码的故障模式:https://dev59.com/w1rUa4cB1Zd3GeqPo_ZL#7308950 - Hans Passant
我在 node.Handle 上遇到了 NullReferenceExeption。对象引用未设置为对象的实例。有什么提示吗? - George Norberg
1
@George,你正在传入“HideCheckBox”函数的Node对象必须为空。该函数并没有做什么特别的事情,只是在使用它。如果你传入一个无效的Node对象,它会崩溃。[相关阅读](https://dev59.com/O2445IYBdhLWcg3w3N6z) - Cody Gray
1
@NathanMcKaskle:虽然已经过去了很多年,但TreeNode.ShowCheckBox不相关的原因是它是一个System.Web.UI.WebControls元素,而不是一个System.Windows.Forms元素(O.P.s的问题是关于System.Windows.Forms的)。 - VA systems engineer
显示剩余3条评论

19

使用TreeViewExtensions。

使用示例:

private void MyForm_Load(object sender, EventArgs e)
{
     this.treeview1.DrawMode = TreeViewDrawMode.OwnerDrawText;
     this.treeview1.DrawNode += new DrawTreeNodeEventHandler(arbolDependencias_DrawNode);
}

void treeview1_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
    if (e.Node.Level == 1) e.Node.HideCheckBox();
    e.DrawDefault = true;
}

这是答案的代码作为扩展方法,使用它您可以做到:

public static class TreeViewExtensions
{
    private const int TVIF_STATE = 0x8;
    private const int TVIS_STATEIMAGEMASK = 0xF000;
    private const int TV_FIRST = 0x1100;
    private const int TVM_SETITEM = TV_FIRST + 63;

    [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
    private struct TVITEM
    {
        public int mask;
        public IntPtr hItem;
        public int state;
        public int stateMask;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string lpszText;
        public int cchTextMax;
        public int iImage;
        public int iSelectedImage;
        public int cChildren;
        public IntPtr lParam;
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
                                             ref TVITEM lParam);

    /// <summary>
    /// Hides the checkbox for the specified node on a TreeView control.
    /// </summary>
    public static void HideCheckBox(this TreeNode node)
    {
        TVITEM tvi = new TVITEM();
        tvi.hItem = node.Handle;
        tvi.mask = TVIF_STATE;
        tvi.stateMask = TVIS_STATEIMAGEMASK;
        tvi.state = 0;
        SendMessage(node.TreeView.Handle, TVM_SETITEM, IntPtr.Zero, ref tvi);
    }
}

在这方面需要谨慎:在我的测试中,调用HideCheckBox会触发节点的重新绘制,导致应用程序陷入无限循环,不断地重新绘制同一个节点。 - John Hall

6

这很好!我唯一会进行的修改是只将TreeNode传递而不是TreeView传递到HideCheckBox方法。可以从TreeNode本身检索TreeView

TreeView tvw = node.TreeView;

0
在任何答案中都没有提到的一件事是,当在选定的节点上按下空格时,复选框可能会重新出现。这意味着我们需要处理TreeView.BeforeCheck事件,以便在状态更改之前取消选中,从而重新添加复选框。
如果我们确切地知道哪些节点隐藏了复选框,那么我们只需添加事件即可完成。这里的e.Action条件只在我们使用IsCheckBoxHidden()来避免在批量检查树视图节点时进行不必要的P/Invoke时才重要。如果我们不使用P/Invoke,那么逻辑足够简单,不值得检查。
private void MyForm_Load(object sender, EventArgs e)
{
    this.treeview1.BeforeCheck += new TreeViewCancelEventHandler(treeview1_BeforeCheck);
}

private void treeview1_BeforeCheck(object sender, TreeViewCancelEventArgs e)
{
    // ByKeyboard is used, since its the one action we don't have control over,
    // We can also handle TreeViewAction.Unknown (for when programmatically checking)
    if (e.Action == TreeViewAction.ByKeyboard)
    {
        // If we know exactly what has its checkbox hidden
        //if (e.Node.Level == 1)
        if (e.Node.IsCheckBoxHidden())
        {
            // Prevent the node from being checked and the checkbox from reappearing
            e.Cancel = true;
        }
    }
}

如果你不能立刻确定哪个节点的复选框被隐藏了,那么我们可以通过更多的P/Invoke来找出来。
private const int TVIF_STATE = 0x8;
private const int TVIS_STATEIMAGEMASK = 0xF000;
private const int TV_FIRST = 0x1100;
private const int TVM_GETITEM = TV_FIRST + 62;

[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
private struct TVITEM
{
    public int mask;
    public IntPtr hItem;
    public int state;
    public int stateMask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpszText;
    public int cchTextMax;
    public int iImage;
    public int iSelectedImage;
    public int cChildren;
    public IntPtr lParam;
}

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
                                         ref TVITEM lParam);

/// <summary>
/// Returns true if the checkbox is hidden for the specified node on a TreeView control.
/// </summary>
public static bool IsCheckBoxHidden(this TreeNode node)
{
    TVITEM tvi = new TVITEM();
    tvi.hItem = node.Handle;
    tvi.mask = TVIF_STATE;
    tvi.stateMask = TVIS_STATEIMAGEMASK;
    SendMessage(node.TreeView.Handle, TVM_GETITEM, IntPtr.Zero, ref tvi);
    // State image values are shifted by 12. 0 = no checkbox, 1 = unchecked, 2 = checked
    var stateImage = tvi.state >> 12;
    return stateImage == 0;
}

或者,你可以使用TVM_GETITEMSTATE代替TVM_GETITEM。两者都能完成任务,但后者需要使用SendMessage的另一个重载函数。
private const int TVM_GETITEMSTATE = TV_FIRST + 39;

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam,
                                         IntPtr lParam);

/// <summary>
/// Returns true if the checkbox is hidden for the specified node on a TreeView control.
/// </summary>
public static bool IsCheckBoxHidden(this TreeNode node)
{
    var stateMask = new IntPtr(TVIS_STATEIMAGEMASK);
    var state = SendMessage(node.TreeView.Handle, TVM_GETITEMSTATE, node.Handle, stateMask).ToInt32();
    // State image values are shifted by 12. 0 = no checkbox, 1 = unchecked, 2 = checked
    var stateImage = state >> 12;
    return stateImage == 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接