我们正在开发一个非常大的.NET WinForms复合应用程序 - 不是CAB,而是一个类似的本土框架。我们正在Windows Server 2003上运行的Citrix和RDP环境中运行。
我们开始遇到随机且难以重现的“错误创建窗口句柄”错误,这似乎是我们的应用程序中的旧时尚句柄泄漏。我们正在大量使用第三方控件(Janus GridEX,Infralution VirtualTree和.NET Magic对接),我们根据数据库中的元数据动态加载和呈现内容。
Google上有很多关于此错误的信息,但没有很多关于如何避免此问题的明确指导。
stackoverflow社区是否对我构建易于操作的winforms应用程序提供了很好的指导?
答案 0 :(得分:28)
我已经跟踪了许多UI未按预期在WinForms中卸载的问题。
以下是一些一般性提示:
答案 1 :(得分:6)
当我将NativeWindow子类化并手动调用CreateHandler时出现此错误。问题是我忘了在我的覆盖版本的WndProc中添加base.WndProc(m)。它导致了同样的错误
答案 2 :(得分:5)
我遇到了这个异常,因为无限循环创建了新的UI控件并设置了它的属性。 在循环多次之后,当变更控制可见属性时抛出此excption。 我发现用户对象和GDI对象(来自任务管理器)都非常大。
我猜您的问题与系统资源被这些UI控件耗尽的原因类似。
答案 3 :(得分:3)
我正在使用Janus Controls。就处置自己而言,他们是非常错误的。我建议您确保它们被正确处理掉。此外,与它们的绑定有时不会释放,因此您必须手动取消绑定对象以处置控件。
答案 4 :(得分:3)
推动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对象,GDI对象和句柄列显示为可见,为此,请选择“查看菜单”>选择列:
完成导致问题的步骤,并观察USER对象数量增加到10,000左右或GDI对象或手柄达到极限。
当您看到对象或句柄增加(通常是显着)时,您可以通过单击“暂停”按钮来暂停Visual Studio中的代码执行。
然后按住F10或F11,观察当对象/处理计数显着增加时的代码。
我到目前为止找到的最好的工具是来自NirSoft的GDIView,它打破了GDI Handle字段:
我将其跟踪到设置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()at System.Windows.Forms.Control.get_Handle()at System.Windows.Forms.ComboBox.InvalidateEverything()at System.Windows.Forms.ComboBox.OnResize(EventArgs e)at System.Windows.Forms.Control.OnSizeChanged(EventArgs e)at System.Windows.Forms.Control.UpdateBounds(Int32 x,Int32 y,Int32 width,Int32 height,Int32 clientWidth,Int32 clientHeight)at System.Windows.Forms.Control.UpdateBounds(Int32 x,Int32 y,Int32 宽度,Int32高度)at System.Windows.Forms.Control.SetBoundsCore(Int32 x,Int32 y,Int32 width,Int32 height,BoundsSpecified specified)at System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x,Int32 y,Int32 width,Int32 height,BoundsSpecified specified)at System.Windows.Forms.Control.SetBounds(Int32 x,Int32 y,Int32 width, Int32高度,BoundsSpecified指定)at System.Windows.Forms.Control.set_Width(Int32 value)
以下是帮助我制定限制的a helpful article by Fabrice的关键所在:
"创建窗口句柄时出错"
当我主动使用为客户端工作的大型Windows窗体应用程序时,用户经常会遇到错误,并且会创建窗口句柄"异常。
除了应用程序消耗太多资源这一事实,这是我们已经解决的一个单独的问题,我们在确定哪些资源耗尽以及这些资源的限制方面遇到了困难。 我们首先考虑关注Windows任务管理器中的Handles计数器。那是因为我们注意到一些流程往往比正常情况下消耗更多这些资源。但是,这个计数器不是好的,因为它跟踪文件,套接字,进程和线程等资源。这些资源被命名为Kernel Objects。
我们应该关注的其他资源是GDI对象和用户对象。您可以在MSDN上查看三类资源的概述。
用户对象
窗口创建问题与用户对象直接相关。
我们尝试确定应用程序可以使用的用户对象的限制。 每个进程有10,000个用户句柄的配额。这个值可以在注册表中更改,但是这个限制并不是我们案例中真正的显示限制。 另一个限制是每个Windows会话66,536个用户句柄。这个限制是理论上的。在实践中,您会注意到无法联系到它。在我们的例子中,我们得到了可怕的"错误创建窗口句柄"当前会话中用户对象总数达到11,000之前的异常。
桌面堆
然后我们发现了真正的罪魁祸首:它是桌面堆#34;
默认情况下,交互式用户会话的所有图形应用程序都在名为" desktop"的内容中执行。分配给此类桌面的资源有限(但可配置)。
注意:用户对象消耗大部分桌面堆的内存空间。这包括窗户。 有关桌面堆的更多信息,您可以参考NTDebugging MSDN博客上发布的非常好的文章:
真正的解决方案是什么?绿色!
增加桌面堆是一种有效的解决方案,但这不是最终的解决方案。真正的解决方案是消耗更少的资源(在我们的例子中减少窗口句柄)。我猜你对这个解决方案有多失望。这真的是我能想到的吗?
嗯,这里没有什么大秘密。唯一的出路就是精益求精。拥有较少复杂的UI是一个良好的开端。它对资源有益,对可用性也有好处。下一步是避免浪费,保护资源,并回收它们!
以下是我们在客户的应用程序中执行此操作的方式:
我们使用TabControls,当它变得可见时,我们动态创建每个标签的内容; 我们使用可扩展/可折叠区域,并且仅在需要时再次使用控件和数据填充它们; 我们尽快释放资源(使用Dispose方法)。当一个地区崩溃时,可以清除它的儿童控制。标签隐藏时相同; 我们使用MVP设计模式,这有助于实现上述目标,因为它将数据与视图分开; 我们使用布局引擎,标准的FlowLayoutPanel和TableLayoutPanel,或者自定义的,而不是创建嵌套面板,GroupBox和Splitters的深层次结构(空的拆分器本身消耗三个窗口句柄......)。 以上只是提示如果您需要构建丰富的Windows窗体屏幕,您可以执行的操作。毫无疑问,您可以找到其他方法。 在我看来,您应该做的第一件事是围绕用例和场景构建应用程序。这有助于仅显示给定时间和给定用户所需的内容。
当然,另一个解决方案是使用一个不依赖句柄的系统...... WPF是谁?
答案 5 :(得分:2)
我在面板中添加控件时遇到了这个异常,因为在面板子控件中没有清除。如果在面板中配置子控件,则修复错误。
For k = 1 To Panel.Controls.Count
Panel.Controls.Item(0).Dispose()
Next
答案 6 :(得分: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.
}
答案 7 :(得分:0)
当我开始在WinForm App中使用线程时发生了相同的错误, 我使用堆栈跟踪来查找引发错误的内容,并发现基础设施的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
但是我的已经具有相同的值。仅调用方法委托对我有用。希望能有所帮助。
答案 8 :(得分:0)
在我的情况下,我覆盖了WndProc(ref Message m)
,但没有调用base.WndProc(ref m)
;