36

We're working on a very large .NET WinForms composite application - not CAB, but a similar home grown framework. We're running in a Citrix and RDP environment running on Windows Server 2003.

We're starting to run into random and difficult to reproduct "Error creating window handle" error that seems to be an old fashion handle leak in our application. We're making heavy use of 3rd Party controls (Janus GridEX, Infralution VirtualTree, and .NET Magic docking) and we do a lot of dynamic loading and rendering of content based on metadata in our database.

There's a lot of info on Google about this error, but not a lot of solid guidance about how to avoid issues in this area.

Does the stackoverflow community have any good guidance for me for building handle-friendly winforms apps?

4

9 回答 9

33

我已经找到了很多关于 UI 没有按预期在 WinForms 中卸载的问题。

以下是一些一般性提示:

  • 很多时候,控件将继续使用,因为控件事件未正确删除(工具提示提供程序在这里给我们造成了非常大的问题)或控件未正确处理。
  • 在所有模态对话框周围使用“使用”块以确保它们被释放
  • 有一些控件属性会在必要之前强制创建窗口句柄(例如设置 TextBox 控件的 ReadOnly 属性将强制实现控件)
  • 使用.Net Memory profiler之类的工具来获取创建的类的计数。此工具的较新版本还将跟踪 GDI 和 USER 对象。
  • 尽量减少使用 Win API 调用(或其他 DllImport 调用)。如果您确实需要使用互操作,请尝试以使 using/Dispose 模式正常工作的方式包装这些调用。
于 2008-09-18T00:44:28.510 回答
11

了解此错误

突破 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/

解决此错误

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

找出造成这么多句柄的最简单方法是打开 TaskMgr.exe。在 TaskMgr.exe 中,您需要使 USER Object、GDI Object 和 Handles 列可见,如图所示,为此选择 View Menu > Select Columns:

在此处输入图像描述

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

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

然后只需按住 F10 或 F11 即可浏览代码,观察对象/句柄计数何时急剧增加。

到目前为止我发现的最好的工具是来自 NirSoft 的 GDIView,它分解了 GDI 句柄字段:

在此处输入图像描述

我将其追踪到设置 DataGridViews“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, Int32 宽度, Int32 高度, Int32 clientWidth, Int32 clientHeight) 在 System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32 width, Int32 height) 在 System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height) , BoundsSpecified 指定) 在 System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x, Int32 y, Int32 宽度, Int32 高度, BoundsSpecified 指定) 在 System.Windows.Forms.Control.SetBounds(Int32 x, Int32 y,Int32 宽度,Int32 高度,BoundsSpecified 指定)在 System.Windows.Forms.Control.set_Width(Int32 值)

以下是Fabrice的一篇有用文章的症结所在,该文章帮助我解决了限制:

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

除了应用程序消耗过多资源(这是我们已经在解决的一个单独的问题)这一事实之外,我们还难以确定哪些资源正在耗尽以及这些资源的限制是什么。我们首先考虑关注 Windows 任务管理器中的 Handles 计数器。那是因为我们注意到某些进程倾向于消耗比正常情况下更多的这些资源。然而,这个计数器并不是一个好的计数器,因为它会跟踪文件、套接字、进程和线程等资源。这些资源被命名为内核对象。

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

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

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

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

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

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

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

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

当然,另一种解决方案是使用不依赖句柄的系统...... WPF 有人吗?

于 2016-10-25T05:30:26.430 回答
9

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

于 2016-12-10T21:22:23.917 回答
5

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

我想您的问题与这些 UI 控件耗尽系统资源的原因类似。

于 2012-01-13T07:12:31.410 回答
4

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

于 2008-09-18T00:47:39.953 回答
3

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

For k = 1 To Panel.Controls.Count
    Panel.Controls.Item(0).Dispose()
Next
于 2016-09-03T05:51:08.860 回答
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.
}
于 2016-09-23T20:58:23.427 回答
0

当我开始在我的 WinForm 应用程序中使用线程时发生了同样的错误,我使用堆栈跟踪来查找引发错误的原因,并发现基础设施的 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 位)在其列表中不可用。另一种解决方案是将桌面堆值更改SharedSection=1024,20480,768为以下位置 HKEY

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

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

于 2019-05-01T18:36:57.553 回答
0

就我而言,我是压倒一切WndProc(ref Message m)但没有打电话base.WndProc(ref m)

于 2020-09-23T14:56:01.957 回答