Found GDI leak in TreeView (anyone know about it?)

G

Greg

I have found what seems to be a leak of GDI resources in the .NET
TreeView. I'm not entirely sure it's the .NET code or the underlying
OS Common control but here's the skinny:

If you use the Checkboxes property and set it to true on a TreeView it
will leak 1 or 2 bitmaps and 2 memDCs every time you create and dispose
of such a treeview.

Our application has a browser-like UI parts of the UI are changing as
controls get removed and destroyed. This leak eventually leaks too
many GDI resources resulting in a crash.

I was using the GDIUsage tool posted at
http://msdn.microsoft.com/msdnmag/issues/03/01/GDILeaks/default.aspx to
verify this and I put together a small app that exhibits the behavior.
Start a new project, create a new form and paste this into your code to
try it out. Click the create a tree, then the middle button to
remove/dispose/GC. You'll notice that the tree with the checkboxes
leaks resources and the one without checkboxes leaks nothing.

We are on VS2k3 using .NET 1.1 SP1.
Do the MS folks know about this and is there a patch?


==================code================
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Panel TreeViewPanel;
private System.Windows.Forms.Button CreateCheckTreeButton;
private System.Windows.Forms.Button CreateNoCheckTreeButton;
private System.Windows.Forms.Button DisposeTreeButton;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.TreeViewPanel = new System.Windows.Forms.Panel();
this.CreateCheckTreeButton = new System.Windows.Forms.Button();
this.CreateNoCheckTreeButton = new System.Windows.Forms.Button();
this.DisposeTreeButton = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// TreeViewPanel
//
this.TreeViewPanel.Location = new System.Drawing.Point(24, 16);
this.TreeViewPanel.Name = "TreeViewPanel";
this.TreeViewPanel.Size = new System.Drawing.Size(680, 400);
this.TreeViewPanel.TabIndex = 0;
//
// CreateCheckTreeButton
//
this.CreateCheckTreeButton.Location = new System.Drawing.Point(24,
440);
this.CreateCheckTreeButton.Name = "CreateCheckTreeButton";
this.CreateCheckTreeButton.Size = new System.Drawing.Size(152, 40);
this.CreateCheckTreeButton.TabIndex = 1;
this.CreateCheckTreeButton.Text = "Create TreeView with CheckBoxes";
this.CreateCheckTreeButton.Click += new
System.EventHandler(this.CreateCheckTreeButton_Click);
//
// CreateNoCheckTreeButton
//
this.CreateNoCheckTreeButton.Location = new
System.Drawing.Point(552, 440);
this.CreateNoCheckTreeButton.Name = "CreateNoCheckTreeButton";
this.CreateNoCheckTreeButton.Size = new System.Drawing.Size(152,
40);
this.CreateNoCheckTreeButton.TabIndex = 2;
this.CreateNoCheckTreeButton.Text = "Create TreeView without
CheckBoxes";
this.CreateNoCheckTreeButton.Click += new
System.EventHandler(this.CreateNoCheckTreeButton_Click);
//
// DisposeTreeButton
//
this.DisposeTreeButton.Location = new System.Drawing.Point(288,
440);
this.DisposeTreeButton.Name = "DisposeTreeButton";
this.DisposeTreeButton.Size = new System.Drawing.Size(152, 40);
this.DisposeTreeButton.TabIndex = 3;
this.DisposeTreeButton.Text = "Remove and Dispose of the Tree";
this.DisposeTreeButton.Click += new
System.EventHandler(this.DisposeTreeButton_Click);
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(728, 510);
this.Controls.Add(this.DisposeTreeButton);
this.Controls.Add(this.CreateNoCheckTreeButton);
this.Controls.Add(this.CreateCheckTreeButton);
this.Controls.Add(this.TreeViewPanel);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);

}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}

private TreeView CreateTreeControl()
{
TreeView tree = new System.Windows.Forms.TreeView();
tree.BorderStyle = System.Windows.Forms.BorderStyle.None;
tree.Dock = System.Windows.Forms.DockStyle.Fill;
tree.ImageIndex = -1;
tree.Location = new System.Drawing.Point(3, 16);
tree.Name = "TheTree";
tree.ShowLines = false;
TreeViewPanel.Controls.Add(tree);

tree.Nodes.Add("Node1");
tree.Nodes.Add("Node2");
tree.Nodes.Add("Node3");
return tree;
}

private void CreateCheckTreeButton_Click(object sender,
System.EventArgs e)
{
TreeView tree = CreateTreeControl();
// Add checkboxes
tree.CheckBoxes = true;
}

private void CreateNoCheckTreeButton_Click(object sender,
System.EventArgs e)
{
CreateTreeControl();
}

private void DisposeTreeButton_Click(object sender, System.EventArgs
e)
{
while (TreeViewPanel.Controls.Count > 0)
{
Control c = TreeViewPanel.Controls[0];
TreeViewPanel.Controls.Remove(c);
c.Dispose();
}

// Invoke the GC
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
==================endcode-=============
 
C

Chris Dunaway

Greg said:
I have found what seems to be a leak of GDI resources in the .NET
TreeView. I'm not entirely sure it's the .NET code or the underlying
OS Common control but here's the skinny:

It would seem it is a bug in the common control Set this llink:

http://tinyurl.com/geff2
 
D

dunko

Thanks for the link. It stinks that its in the common control. You'd
think they would include this type of fix in a windows update patch but
oh well.
 
D

dunko

Ok I did a little more research on this. Apparently this is *not* a
bug in the underlying control but rather the .NET library. As stated
in some other threads here the SDK specifically mentions this:
========================
TVS_CHECKBOXES
Once a tree-view control is created with this style, the style cannot
be removed. Instead, you must destroy the control and create a new one
in its place. Destroying the tree-view control does not destroy the
check box state image list. You must destroy it explicitly. Get the
handle to the state image list by sending the tree-view control a
TVM_GETIMAGELIST message. Then destroy the image list with
ImageList_Destroy.
========================
(at
ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.1033/shellcc/platform/commctls/treeview/styles.htm
if you have MSDN installed)

So the leak is because the .NET library doesn't properly handle this
when the TreeView is Disposed(). Using reflector, this is the code
that sits in the Dispose method of the TreeView:

====================
protected override void Dispose(bool disposing)
{
if (disposing)
{
lock (this)
{
if (this.imageList != null)
{
this.imageList.Disposed -= new
EventHandler(this.DetachImageList);
this.imageList = null;
}
}
}
base.Dispose(disposing);
}
====================


So in short this is a bug in the .NET library. I don't have .NET 2.0
to be able to tell if they have fixed this or not, does anyone else
know?

Here is the code someone suggested for handling this in C++. This is
not available through the .NET interface though.

==================
CTreeCtrl * pT = (CTreeCtrl *) GetDlgItem( IDC_TREE1 );
pI = pT->GetImageList( TVSIL_STATE );
if ( pI )
{
pI->DeleteImageList();
}
==================
 
D

dunko

Ok I got annoyed and came up with a workaround. It's a derived class
that eliminates the leak. I'll share it here in case it helps anyone
else out.

Hooray for PInvoke.

=================
using System;
using System.Runtime.InteropServices;
using System.Reflection;

namespace TreeViewCheckLeak
{
/// <summary>
/// This class is a hack workaround due to a bug in the .NET
libraries. The problem is outlined at
///
http://groups.google.com/group/micr...b362?lnk=st&q=TreeView GDI leak&rnum=1&hl=en&
/// The TreeView leaks GDI resources (actually it leaks an ImageList)
if checkboxes are turned on.
/// </summary>
public class LeaklessTreeView : System.Windows.Forms.TreeView
{
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_Destroy(IntPtr hImageList);

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

// Constants taken from the Win32 API
const uint TV_FIRST = 0x1100;
const uint TVM_GETIMAGELIST = TV_FIRST + 8;
// The imagelist to get the from the treeview control
const int TVSIL_NORMAL = 0;
const int TVSIL_STATE = 2;

/// <summary>
/// Required by the visual designer
/// </summary>
private void InitializeComponent()
{
//
// LeaklessTreeView
//
this.StyleChanged += new
System.EventHandler(this.LeaklessTreeView_StyleChanged);
}

/// <summary>
/// The handle to the state ImageList
/// </summary>
private IntPtr _checkboxImageList = IntPtr.Zero;

/// <summary>
/// Creates a TreeView without an annoying leak
/// </summary>
public LeaklessTreeView() : base()
{
InitializeComponent();
}


private void LeaklessTreeView_StyleChanged(object sender,
System.EventArgs e)
{
// Get the ImageList handle for the State (checkbox) image list if
we are using checkboxes.
if (this.CheckBoxes)
{
IntPtr handle = SendMessage(this.Handle, TVM_GETIMAGELIST, new
IntPtr(TVSIL_STATE), IntPtr.Zero);
if (handle != IntPtr.Zero)
_checkboxImageList = handle;
}
}


protected override void Dispose(bool disposing)
{
if (_checkboxImageList != IntPtr.Zero)
{
// Blow away the image list
ImageList_Destroy(_checkboxImageList);
}
base.Dispose(disposing);
}
}
}
=================
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top