如何在取消关闭时重置关闭原因

时间:2014-05-26 14:58:29

标签: vb.net winforms .net-4.0

问题

  

是否可以在模式对话框的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.AcceptButtonForm.CancelButtonIButtonControl),则关闭原因将设置为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消息,但它所做的就是设置私有字段。

所以我钻研了反射器并跟着堆栈。以下图片是高度简化的插图。

Stack

这就是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。如果单击按钮是用户操作的结果,那么是什么?

3 个答案:

答案 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值很重要,有些非常准确,有些只是猜测:

  • CloseReason.WindowsShutdown - 可靠
  • CloseReason.ApplicationExitCall - 可靠
  • CloseReason.MdiFormClosing - 可靠,不太有用
  • CloseReason.FormOwnerClosing - 可靠,不太有用
  • CloseReason.TaskManagerClosing - 完全猜测,将在任何程序发送WM_CLOSE消息时返回,而不仅仅是任务管理器
  • CloseReason.UserClosing - 完全猜测,当程序调用Close()方法时也会返回
  • CloseReason.None - 它只是不知道。

是的,当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。关于这方面的文件似乎有点模糊。