问题
是否可以在模式对话框的CloseReason事件中重置FormClosingEventArgs提供的FormClosing?
症状
如果先前已取消关闭事件,则设置模式对话框的DialogResult
可能会导致"不正确" CloseReason
。
详情
(以下代码只是示例代码,以突出不便之处)
想象一下,我有一个带有两个按钮的表单,确定和取消,显示为模式对话框。
Me.btnOk = New Button With {.DialogResult = Windows.Forms.DialogResult.OK}
Me.btnCancel = New Button With {.DialogResult = Windows.Forms.DialogResult.Cancel}
Me.AcceptButton = Me.btnOk
Me.CancelButton = Me.btnCancel
任何关闭表单的尝试都将被取消。
如果按以下顺序点击每个按钮(包括[X]
- 关闭表单按钮),则结果如下:
案例1
btnOk
::::::::::: 无 btnCancel
::: 无 X
::::::::::::::::::: UserClosing 现在,如果我重复这些步骤,您会发现UserClosing
原因仍然存在:
btnOk
::::::::::: UserClosing btnCancel
::: UserClosing X
::::::::::::::::::: UserClosing 案例2
X
::::::::::::::::::: UserClosing btnCancel
::: UserClosing btnOk
::::::::::: UserClosing 同样在这里。点击X
按钮后,关闭原因将始终返回UserClosing
。
示例应用
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.Text = "Test"
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog
Me.MinimizeBox = False
Me.MaximizeBox = False
Me.ClientSize = New Size(75, 25)
Me.StartPosition = FormStartPosition.CenterScreen
Me.btnOpenDialog = New Button() With {.TabIndex = 0, .Dock = DockStyle.Fill, .Text = "Open dialog"}
Me.Controls.Add(Me.btnOpenDialog)
End Sub
Private Sub HandleOpenDialog(sender As Object, e As EventArgs) Handles btnOpenDialog.Click
Using instance As New CustomDialog()
instance.ShowDialog()
End Using
End Sub
Private WithEvents btnOpenDialog As Button
Private Class CustomDialog
Inherits Form
Public Sub New()
Me.Text = "Custom dialog"
Me.ClientSize = New Size(400, 200)
Me.StartPosition = FormStartPosition.CenterParent
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog
Me.MinimizeBox = False
Me.MaximizeBox = False
Me.tbOutput = New RichTextBox() With {.TabIndex = 0, .Bounds = New Rectangle(0, 0, 400, 155), .ReadOnly = True, .ScrollBars = RichTextBoxScrollBars.ForcedBoth, .WordWrap = True}
Me.btnExit = New Button With {.TabIndex = 3, .Text = "Exit", .Bounds = New Rectangle(10, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Left)}
Me.btnOk = New Button With {.TabIndex = 1, .Text = "OK", .Bounds = New Rectangle(237, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.OK}
Me.btnCancel = New Button With {.TabIndex = 2, .Text = "Cancel", .Bounds = New Rectangle(315, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.Cancel}
Me.Controls.AddRange({Me.tbOutput, Me.btnExit, Me.btnOk, Me.btnCancel})
Me.AcceptButton = Me.btnOk
Me.CancelButton = Me.btnCancel
End Sub
Private Sub HandleExitDialog(sender As Object, e As EventArgs) Handles btnExit.Click
Me.exitPending = True
Me.Close()
End Sub
Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
If (Not Me.exitPending) Then
e.Cancel = True
Me.tbOutput.Text += (String.Format("DialogResult={0}, CloseReason={1}{2}", Me.DialogResult.ToString(), e.CloseReason.ToString(), Environment.NewLine))
Me.DialogResult = Windows.Forms.DialogResult.None
End If
MyBase.OnFormClosing(e)
End Sub
Private exitPending As Boolean
Private WithEvents btnExit As Button
Private WithEvents btnCancel As Button
Private WithEvents btnOk As Button
Private WithEvents tbOutput As RichTextBox
End Class
End Class
我的印象是,如果单击Form.AcceptButton
或Form.CancelButton
(IButtonControl),则关闭原因将设置为UserClosing
,但这不是案件。在以下代码中,您将看到它所做的只是将拥有表单的DialogResult
设置为其自己的DialogResult
。
Protected Overrides Sub OnClick(ByVal e As EventArgs)
Dim form As Form = MyBase.FindFormInternal
If (Not form Is Nothing) Then
form.DialogResult = Me.DialogResult
End If
MyBase.AccessibilityNotifyClients(AccessibleEvents.StateChange, -1)
MyBase.AccessibilityNotifyClients(AccessibleEvents.NameChange, -1)
MyBase.OnClick(e)
End Sub
Control
类做有一个名为CloseReason
的属性,但它被定义为Friend
,因此无法访问。
我还认为设置表单DialogResult
会导致发送WM
消息,但它所做的就是设置私有字段。
所以我钻研了反射器并跟着堆栈。以下图片是高度简化的插图。
这就是CheckCloseDialog
方法的样子:
Friend Function CheckCloseDialog(ByVal closingOnly As Boolean) As Boolean
If ((Me.dialogResult = DialogResult.None) AndAlso MyBase.Visible) Then
Return False
End If
Try
Dim e As New FormClosingEventArgs(Me.closeReason, False)
If Not Me.CalledClosing Then
Me.OnClosing(e)
Me.OnFormClosing(e)
If e.Cancel Then
Me.dialogResult = DialogResult.None
Else
Me.CalledClosing = True
End If
End If
If (Not closingOnly AndAlso (Me.dialogResult <> DialogResult.None)) Then
Dim args2 As New FormClosedEventArgs(Me.closeReason)
Me.OnClosed(args2)
Me.OnFormClosed(args2)
Me.CalledClosing = False
End If
Catch exception As Exception
Me.dialogResult = DialogResult.None
If NativeWindow.WndProcShouldBeDebuggable Then
Throw
End If
Application.OnThreadException(exception)
End Try
If (Me.dialogResult = DialogResult.None) Then
Return Not MyBase.Visible
End If
Return True
End Function
正如您所看到的,模态消息循环在每个周期中检查DialogResult
,如果条件满足,它将在创建时使用存储的 CloseReason
(如所观察到的) FormClosingEventArgs
。
摘要
是的,我知道IButtonControl
接口有一个PerformClick
方法,您可以通过编程方式调用,但IMO仍然会闻起来像一个bug。如果单击按钮不是用户操作的结果,那么是什么?
答案 0 :(得分:6)
理解为什么这样做会非常重要,当你过分依赖CloseReason时,你可能会遇到麻烦。这不是一个错误,由于Windows的设计方式,这是一个限制。一个核心问题是WM_CLOSE message的制定方式,它是设置列车运行的方式,首先触发FormClosing事件。
此消息可以发送 lot 的原因,您熟悉常见的消息。但这并不是它结束的地方,其他程序也可以发送该消息。你可以告诉&#34;缺陷&#34;从我链接到的MSDN Library文章中,消息缺少编码消息的 intent 的WPARAM值。因此,程序没有任何方法可以向您提供合理的CloseReason。 Winforms因某种原因被迫猜测。这当然是一个完全不完美的猜测。
那不是它结束的地方,DialogResult属性也是一个问题。当任何代码分配该属性时,它将强制关闭对话框。但同样的问题,这样的代码没有任何方式来指示赋值的 intent 。所以它没有,它在内部Form.CloseReason属性中保留它之前的任何值,默认为None。
这是&#34;正确&#34;在.NET 1.0中实现,只有Closing事件,并没有给出任何理由。但是,这并没有那么好用,使用它的应用程序长期阻止Windows关闭。他们只是不知道显示一个消息框是不合适的。添加了.NET 2.0 FormClosing事件作为解决方法。但它需要处理不完美的猜测。
评估CloseReason值很重要,有些非常准确,有些只是猜测:
是的,当FormClosing事件处理程序取消时,Winforms没有将CloseReason设置回None,这可能是一个错误。但这并不是真正重要的那种错误。因为无论如何你都不能区别对待UserClosing和None。
答案 1 :(得分:2)
我可能会称这是一个错误。
正如您所提到的,CloseReason属性标记为internal(或VB.Net术语中的Friend),因此解决该问题的方法是使用Reflection自行重置该值:
Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
If Not exitPending Then
e.Cancel = True
tbOutput.AppendText(String.Format("DialogResult={0}, CloseReason={1}{2}", _
Me.DialogResult.ToString(), e.CloseReason.ToString(), _
Environment.NewLine))
Dim pi As PropertyInfo
pi = Me.GetType.GetProperty("CloseReason", _
BindingFlags.Instance Or BindingFlags.NonPublic)
pi.SetValue(Me, CloseReason.None, Nothing)
End If
MyBase.OnFormClosing(e)
End Sub
不保证此代码可以在WinForms的未来版本上运行,但我猜这些天是安全的选择。 : - )
答案 2 :(得分:-1)
Private Const WM_SYSCOMMAND As Int32 = &H112
Private Const SC_CLOSE As Int32 = &HF060
'Private Const SC_MAXIMIZE As Int32 = &HF030
'Private Const SC_MINIMIZE As Int32 = &HF020
'Private Const SC_RESTORE As Int32 = &HF120
Private _commandClose As Boolean = False
Protected Overrides Sub WndProc(ByRef m As Message)
If CInt(m.Msg) = WM_SYSCOMMAND Then
If (m.WParam.ToInt32 And &HFFF0) = SC_CLOSE Then _commandClose = True
End If
MyBase.WndProc(m)
End Sub
Private Sub baseClick(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Click
Close()
End Sub
Protected Overrides Sub OnFormClosing(ByVal e As FormClosingEventArgs)
If _commandClose Then DialogResult = ' ...
MyBase.OnFormClosing(e)
End Sub
参考:MSDN - WM_SYSCOMMAND message
嗯,实际上,这确实有效。但是与官方文档不同,SC_CLOSE
会激活Alt + F4等等,即使没有特别提及。
调用Form.Close()
方法时不会触发。因此,按预期工作。
但是,如果您按照设计调用UserClosing
方法,它仍会返回Close()
。
注意:SC_SCREENSAVE
可用于检测/阻止屏保,以及SC_MONITORPOWER
。关于这方面的文件似乎有点模糊。