“创建窗口句柄时出错”

问题描述 投票:25回答:8

我们正在开发一个非常大的.NET WinForms复合应用程序 - 不是CAB,而是一个类似的本土框架。我们正在运行在Windows Server 2003上的Citrix和RDP环境中运行。

我们开始遇到随机和难以复制的“错误创建窗口句柄”错误,这似乎是我们的应用程序中的旧时尚句柄泄漏。我们正在大量使用第三方控件(Janus GridEX,Infralution VirtualTree和.NET Magic对接),我们根据数据库中的元数据进行了大量动态加载和内容呈现。

谷歌有很多关于这个错误的信息,但没有很多关于如何避免这方面问题的可靠指导。

stackoverflow社区是否对我构建易于操作的winforms应用程序有任何良好的指导?

.net winforms handles
8个回答
28
投票

我已经跟踪了很多问题,因为在WinForms中没有像预期的那样卸载UI。

以下是一些一般提示:

  • 很多时候,控件将继续使用,因为控件事件没有被正确删除(工具提示提供程序在这里导致我们真正的大问题)或控件没有正确处理。
  • 在所有模态对话框周围使用“使用”块以确保它们是Disposed
  • 有一些控件属性会在必要之前强制创建窗口句柄(例如,设置TextBox控件的ReadOnly属性将强制实现控件)
  • 使用像.Net Memory profiler这样的工具来获取创建的类的计数。此工具的较新版本还将跟踪GDI和USER对象。
  • 尽量减少使用Win API调用(或其他DllImport调用)。如果确实需要使用interop,请尝试以这样的方式包装这些调用,即using / Dispose模式将正常工作。

6
投票

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


5
投票

我遇到了这个异常,因为无限循环创建了新的UI控件并设置了它的属性。在循环多次之后,当变更控制可见属性时抛出此excption。我发现用户对象和GDI对象(来自任务管理器)都非常大。

我想你的问题是类似的原因,系统资源被那些UI控件耗尽。


3
投票

我正在使用Janus Controls工作。就处置自己而言,他们是非常错误的。我建议您确保它们被正确处理掉。此外,与它们的绑定有时不会释放,因此您必须手动取消绑定对象以处置控件。


3
投票

Understanding this error

推动Windows的局限:USER和GDI对象 - Mark Russinovich的第1部分:https://blogs.technet.microsoft.com/markrussinovich/2010/02/24/pushing-the-limits-of-windows-user-and-gdi-objects-part-1/

Troubleshooting this error

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

找出创建这么多句柄的最简单方法是打开TaskMgr.exe。在TaskMgr.exe中,您需要将USER对象,GDI对象和句柄列显示为可见,为此,请选择“视图”菜单>“选择列”:

enter image description here

完成导致问题的步骤并观察USER对象计数增加到大约10,000或GDI对象或句柄达到其极限。

当您看到对象或句柄增加(通常是显着)时,您可以通过单击“暂停”按钮来停止Visual Studio中的代码执行。

然后按住F10或F11以巡航代码,观察对象/句柄计数何时显着增加。

到目前为止我找到的最好的工具是来自NirSoft的GDIView,它打破了GDI Handle字段:

enter image description here

我将其跟踪到设置DataGridViews列宽时使用的代码:

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

这是堆栈跟踪:

在System.Windows.Forms.Control.CreateHandle()处的System.Windows.Forms.ComboBox.CreateHandle()处于System.Windows.Forms.Control.get_Handle()处于System.Windows.Forms.ComboBox.InvalidateEverything()处于System System.Windows.Forms.Control.UpdateBounds(Int32 x,Int32 y,Int32 width,Int32 height,Int32)的System.Windows.Forms.Control.OnSizeChanged(EventArgs e)中的.Windows.Forms.ComboBox.OnResize(EventArgs e)在System.Windows.Forms.Control.SetBoundsCore(Int32 x,Int32 y,Int32 width,Int32 height)的System.Windows.Forms.Control.UpdateBounds(Int32 x,Int32 y,Int32 width,Int32 height)处的clientWidth,Int32 clientHeight) System.Windows.Forms.Control.SetBounds(Int32 x,Int32 y,Int32 width,System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x,Int32 y,Int32 width,Int32 height,BoundsSpecified指定),在System.Windows.Forms.Control.SetBounds(Int32 x,Int32 y,Int32 width,在System.Windows.Forms.Control.set_Width(Int32值)的Int32高度,BoundsSpecified指定)

这是a helpful article by Fabrice的关键,帮助我解决了极限:

“创建窗口句柄时出错” 当我正在为客户端工作的大型Windows窗体应用程序被主动使用时,用户经常会收到“创建窗口句柄错误”异常。

除了应用程序消耗太多资源这一事实,这是我们已经解决的一个单独的问题,我们在确定哪些资源耗尽以及这些资源的限制方面遇到了困难。我们首先考虑关注Windows任务管理器中的Handles计数器。那是因为我们注意到一些流程往往比正常情况下消耗更多这些资源。但是,这个计数器不是好的,因为它跟踪文件,套接字,进程和线程等资源。这些资源名为Kernel Objects。

我们应该关注的其他类型的资源是GDI对象和用户对象。您可以在MSDN上查看三类资源的概述。

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

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

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

注意:用户对象消耗大部分桌面堆的内存空间。这包括窗户。有关Desktop Heap的更多信息,您可以参考NTDebugging MSDN博客上发布的非常好的文章:

什么是真正的解决方案?绿色! 增加桌面堆是一种有效的解决方案,但这不是最终的解决方案。真正的解决方案是消耗更少的资源(在我们的例子中减少窗口句柄)。我猜你对这个解决方案有多失望。这真的是我能想到的吗?嗯,这里没有什么大秘密。唯一的出路就是精益求精。拥有较少复杂的UI是一个良好的开端。这对资源有利,对可用性也有好处。下一步是避免浪费,保护资源,并回收它们!

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

我们使用TabControls,当它变得可见时,我们动态创建每个标签的内容;我们使用可扩展/可折叠区域,并且仅在需要时再次使用控件和数据填充它们;我们尽快释放资源(使用Dispose方法)。当某个区域折叠时,可以清除它的子控件。标签隐藏时相同;我们使用MVP设计模式,这有助于实现上述目标,因为它将数据与视图分开;我们使用布局引擎,标准的FlowLayoutPanel和TableLayoutPanel,或者自定义的,而不是创建嵌套面板,GroupBox和Splitters的深层次结构(空的拆分器本身消耗三个窗口句柄......)。以上只是提示如果您需要构建丰富的Windows窗体屏幕,您可以执行的操作。毫无疑问,您可以找到其他方法。在我看来,您应该做的第一件事是围绕用例和场景构建应用程序。这有助于仅显示给定时间和给定用户所需的内容。

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


2
投票

我在面板中添加控件时遇到了这个异常,因为在面板子控件中没有清除。如果在面板中配置子控件然后bug修复。

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

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
投票

当我开始在我的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

但我的价值已经相同了。只调用方法委托为我工作。希望这有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.