创建窗口句柄出错。

37
我们正在开发一个非常大的.NET WinForms组合应用程序 - 不是CAB,而是一个类似于自行开发的框架。我们在运行Windows Server 2003上的Citrix和RDP环境中。我们开始遇到随机和难以重现的“Error creating window handle”错误,这似乎是我们应用程序中旧式句柄泄漏的问题。我们大量使用第三方控件(Janus GridEX、Infralution VirtualTree和.NET Magic docking),并根据数据库中的元数据进行大量动态加载和呈现内容。关于这个错误,谷歌上有很多信息,但没有太多关于如何避免这个问题的实质性指导。请问stackoverflow社区对于我构建友好的WinForms应用程序有哪些好的建议?

1
请参阅我的[有关“创建窗口句柄错误”的帖子][1],了解它与用户对象和桌面堆的关系,并提供一些解决方案。 [1]: http://weblogs.asp.net/fmarguerie/archive/2009/08/07/cannot-create-window-handle-desktop-heap.aspx - Fabrice
11个回答

33

我在WinForms中跟踪了很多UI未按预期卸载的问题。

以下是一些总体提示:

  • 大部分情况下,控件仍会保持在使用状态,因为控件事件没有正确地被移除(其中,提示工具提供程序在这里导致了非常大的问题),或者控件没有正确地被Dispose。
  • 在所有模态对话框周围使用"using"块,以确保它们被Dispose。
  • 有一些控件属性会在必要之前强制创建窗口句柄(例如,将TextBox控件的ReadOnly属性设置为true会强制实现该控件)。
  • 使用像.Net内存分析器这样的工具来获取已创建类的数量。较新版本的此工具还将跟踪GDI和USER对象。
  • 尽量减少对Win API调用(或其他DllImport调用)的使用。如果确实需要使用Interop,请尝试以正确的方式包装这些调用,使得使用/Dispose模式能够正常工作。

12

理解此错误

突破Windows极限:USER和GDI对象-第1部分,作者Mark Russinovich: https://blogs.technet.microsoft.com/markrussinovich/2010/02/24/pushing-the-limits-of-windows-user-and-gdi-objects-part-1/

解决此错误

您需要能够重现问题。以下是记录这样做步骤的一种方法:https://stackoverflow.com/a/30525957/495455

最简单的确定创建如此多句柄的方法是打开TaskMgr.exe。在TaskMgr.exe中,需要将USER Object、GDI Object和Handles列显示出来,如下所示,要执行此操作,请选择View菜单> Select Columns:

enter image description here

按照引起问题的步骤,观察USER Object计数增加到约10,000个或GDI Objects或Handles达到其极限。

当您看到Object或Handles增加(通常会显著增加)时,可以通过单击Pause按钮在Visual Studio中停止代码执行。

然后只需按住F10或F11浏览代码,看到Object/Handle计数显著增加时即可。

到目前为止我发现的最好的工具是来自NirSoft的GDIView,它将GDI Handle字段拆分:

enter image description here

我把它追溯到DataGridView设置“Filter Combobox”列位置和宽度时使用的此代码:

If Me.Controls.ContainsKey(comboName) Then
    cbo = CType(Me.Controls(comboName), ComboBox)
    With cbo
        .Location = New System.Drawing.Point(cumulativeWidth, 0)
        .Width = Me.Columns(i).Width
    End With
    'Explicitly cleaning up fixed the issue of releasing USER objects.
    cbo.Dispose()
    cbo = Nothing  
End If

在我的情况下(如上所述),显式释放和清理解决了释放USER对象的问题。

以下是堆栈跟踪:

在System.Windows.Forms.Control.CreateHandle() 中 在System.Windows.Forms.ComboBox.CreateHandle() 中 在System.Windows.Forms.Control.get_Handle() 中 在System.Windows.Forms.ComboBox.InvalidateEverything() 中 在System.Windows.Forms.ComboBox.OnResize(EventArgs e) 中 在System.Windows.Forms.Control.OnSizeChanged(EventArgs e) 中 在System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32width, Int32 height, Int32 clientWidth, Int32 clientHeight) 中 在System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32width, Int32 height) 中 在System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32width, Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x, Int32 y, Int32width, Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.Control.SetBounds(Int32 x, Int32 y, Int32 width,Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.Control.set_Width(Int32 value)

以下是Fabrice的一篇有用文章的核心部分,该文章帮助我弄清楚了限制:

"Error creating window handle"
我正在为客户开发的一个大型Windows Forms应用程序在被积极使用时,用户经常会遇到"Error creating window handle"异常。

除了应用程序消耗过多资源这个单独的问题,我们还难以确定哪些资源正在耗尽以及这些资源的限制是什么。 我们最初考虑关注Windows任务管理器中的处理器计数器。那是因为我们注意到一些进程倾向于消耗比正常情况下更多的这些资源。然而,这个计数器不是正确的,因为它跟踪像文件、套接字、进程和线程这样的资源。这些资源被称为内核对象。

我们应该关注的另一种资源类型是GDI(图形设备接口)对象和用户对象。您可以在MSDN上获取这三类资源的概述。

用户对象
窗口创建问题直接与用户对象有关。

我们尝试确定应用程序可以使用多少个用户对象的限制。 每个进程有10,000个用户句柄的配额。这个值可以在注册表中更改,但在我们的情况下,这个限制并不是真正的阻碍因素。 另一个限制是每个Windows会话的66,536个用户句柄。这个限制是理论上的。实际上,您会注意到它无法达到。在我们的情况下,在当前会话中的用户对象总数达到11,000之前,我们就遇到了可怕的“创建窗口句柄错误”异常。

桌面堆
然后,我们发现真正的罪魁祸首是“桌面堆”。 默认情况下,交互式用户会话的所有图形应用程序都在所谓的“桌面”中执行。分配给这样的桌面的资源是有限的(但可配置)。

注意:用户对象是消耗桌面堆内存空间的主要对象。这包括窗口。 有关桌面堆的更多信息,请参考NTDebugging MSDN博客上发布的非常好的文章:

真正的解决方案是什么?做绿色应用!
增加桌面堆是一种有效的解决方案,但不是最终解决方案。真正的解决方案是消耗更少的资源(在我们的情况下是较少的窗口句柄)。我可以猜测您对这个解决方案会感到非常失望。这真的是我能想出的全部吗? 好吧,在这里没有什么大秘密。唯一的出路就是要简洁。拥有更简单的用户界面是一个很好的开始。这对资源和可用性都有好处。接下来的步骤是避免浪费,保护资源并回收它们!

以下是我们在客户应用程序中执行此操作的方式:

我们使用TabControls,并在其变为可见时动态创建每个选项卡的内容; 我们使用可扩展/可折叠区域,并在需要时再填充控件和数据; 尽快释放资源(使用Dispose方法)。当一个区域被折叠时,可以清除其子控件。当选项卡变为隐藏状态时也是如此; 我们使用MVP设计模式,它有助于使上述操作成为可能,因为它将数据与视图分离; 我们使用布局引擎,标准的FlowLayoutPanel和TableLayoutPanel以及自定义的引擎,而不是创建深层次的嵌套面板、GroupBoxes和Splitters(空Splitters本身就消耗了三个窗口句柄...)。 以上只是提示,如果您需要构建丰富的Windows Forms屏幕,您可以做什么。毫无疑问,您可以找到其他方法。 在我看来,您应该首先基于用例和场景构建应用程序。这有助于仅在给定时间和给定用户需要时显示所需内容。

当然,另一个解决方案是使用不依赖于句柄的系统...... WPF呢?


9

当我手动调用CreateHandler时,我在子类化NativeWindow时遇到了这个错误。问题是我忘记在我的重写版本的WndProc中添加base.WndProc(m)。这导致了相同的错误。


1
我也曾经遇到过类似的健忘症状,这个解决方案提醒了我回去检查。问题解决了。谢谢。 - Yonabart
@Yonabart,很高兴能帮忙 :) - aderesh
我修改了一个重写的WindProc版本,使其在加载时不执行base.WndProc(m),这导致了问题。 - SimonKravis

5

我遇到了这个异常,因为我创建了许多UI控件并设置了它们的属性,导致了无限循环。当改变控件的可见性属性时,抛出了这个异常。我发现用户对象和GDI对象(从任务管理器中查看)都非常大。

我猜测你的问题可能是由于系统资源被这些UI控件耗尽所导致的。


4

我在工作中使用Janus Controls。就控件自我处理而言,它们非常容易出现错误。我建议您确保正确处理它们的处置。另外,有时与它们绑定的对象无法释放,因此您必须手动解除对象绑定以处置控件。


3

在向面板添加控件时,我遇到了这个异常,因为面板中的子控件没有清除。如果清除面板中的子控件,则可以修复该错误。

For k = 1 To Panel.Controls.Count
    Panel.Controls.Item(0).Dispose()
Next

谢谢,我也遇到了同样的情况。我有一个包含多个控件的面板。 - CABascourt

0

我遇到了相同的 .Net 运行时错误,但我的解决方案不同。

我的情况: 从一个返回 DialogResult 的弹出式对话框中,用户会单击一个按钮来发送电子邮件。我添加了一个线程,以便在后台生成报告时 UI 不会锁定。这种情况最终导致了那个不寻常的错误消息。

导致问题的代码: 这段代码的问题在于线程立即启动并返回,导致 DialogResult 被返回并且在线程能够正确从字段中获取值之前关闭了对话框。

private void Dialog_SendEmailSummary_Button_Click(object sender, EventArgs e)
{
    SendSummaryEmail();
    DialogResult = DialogResult.OK;
}

private void SendSummaryEmail()
{
    var t = new Thread(() => SendSummaryThread(Textbox_Subject.Text, Textbox_Body.Text, Checkbox_IncludeDetails.Checked));
    t.Start();
}

private void SendSummaryThread(string subject, string comment, bool includeTestNames)
{
    // ... Create and send the email.
}

针对此情况的解决方案: 解决方案是在将值传递给创建线程的方法之前抓取并存储这些值。

private void Dialog_SendEmailSummary_Button_Click(object sender, EventArgs e)
{
    SendSummaryEmail(Textbox_Subject.Text, Textbox_Body.Text, Checkbox_IncludeDetails.Checked);
    DialogResult = DialogResult.OK;
}

private void SendSummaryEmail(string subject, string comment, bool includeTestNames)
{
    var t = new Thread(() => SendSummaryThread(subject, comment, includeTestNames));
    t.Start();
}

private void SendSummaryThread(string subject, string comment, bool includeTestNames)
{
    // ... Create and send the email.
}

0
您好, 我遇到了同样的问题,由于大量PictureBox用于图像处理被添加到面板中。原始代码使用Controls.RemoveAt(0)仍然会导致问题。只需更改为Controls[0].Dispose -> 似乎问题就解决了。
while (mGridPanel.Controls.Count > 0)
    mGridPanel.Controls[0].Dispose();

0

当我在我的WinForm应用程序中开始使用线程时,发生了相同的错误, 我使用堆栈跟踪查找是什么导致错误,并发现Infragistics的UltraDesktopAlert组件是原因,所以我以不同的方式调用它,现在错误已经消失。

 this.Invoke((MethodInvoker)delegate
{
    //call your method here
});

完整的代码将是这样。

private void ultraButton1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() => myMethod1());
}

void myMethod1()
{
    //my logic

    this.Invoke((MethodInvoker)delegate
    {
        ultraDesktopAlert1.Show($"my message header", "my message");
    });

    //my logic
}

我也无法使用GDI实用程序来查找我的应用程序创建了多少句柄,但我的应用程序(64位)未出现在其列表中。 另一个解决方案是在以下位置HKEY更改桌面堆值为SharedSection=1024,20480,768

Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems

但我的已经具有相同的值。只有调用方法委托对我有用。希望这可以帮到你。


0

我在我的C# Windows服务中遇到了同样的错误。在我的服务中,在OnStart()方法中,我使用“using”语句创建了一个新的WinForms对象。就在那之后,我的服务就会抛出这个错误。

我的解决方法很简单,事实证明,如果我使用“本地系统”作为登录方式启动我的服务,它就会抛出这个错误,但是如果我使用帐户类型启动我的服务,一切都很好。

我希望有人会发现这个有用。

enter image description here


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