VBA - 正确销毁无模式用户窗体实例

VBA - destroy a modeless UserForm instance properly

简介:

我知道 - 显示用户表单 - 最好的做法是

有用link

出色的概述 "UserForm1.Show?" 可在 https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/ 找到 以及许多示例性 SO 答案(感谢 Mathieu Guindon 又名 Mat's Mug 和 RubberDuck)。

进一步选择(►2019 年 5 月 1 日编辑


1) 模态用户窗体的工作示例

据我所知——我确实在努力学习——以下代码对于 modal UF 应该没问题:

案例 1a) .. 带有一个 局部变量 用于 UF 实例,如常见的那样:

Public Sub ShowFormA
  Dim ufA As UserForm1
  Set ufA = New UserForm1
' show userform 
  ufA.Show          ' equivalent to: ufA.Show vbModal

' handle data after user okay
  If Not ufA.IsCancelled Then
      '  do something ...
  End If

' >> object reference destroyed expressly (as seen in some examples)
  unload ufA
End Sub

案例 1b) .. 没有局部变量,但使用 With New 代码块:

' ----------------------------------------------------------
' >> no need to destruct object reference expressly,
'    as it will be destroyed whenever exiting the with block
' ----------------------------------------------------------
  With New UserForm1
      .Show         ' equivalent to: ufA.Show vbModal

    ' handle data after user okay
      If Not .IsCancelled Then
      '  do something ...
      End If
  End With

2) 问题

使用 MODELESS UserForm 实例时出现问题。

好的,with 块方法(参见 1b)应该足以在 x-iting 之后销毁任何对象引用:

  With New UserForm1
      .Show vbModeless  ' << show modeless uf
  End With

如果我尝试,但是

由于表单是无模式的,所有代码行将立即执行:


3) 问题

我如何处理 a) 通过无模式窗体的调用代码正确报告的用户窗体取消,以及 b) 如果使用局部变量(必要?)卸载?

我通常将无模式用户窗体实例的生命周期与工作簿的生命周期联系起来,方法是将代码放在 ThisWorkbook 后面:

Option Explicit

Private m_MyForm As UserForm1

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    If Not m_MyForm Is Nothing Then
        Unload m_MyForm
        Set m_MyForm = Nothing
    End If
End Sub

Friend Property Get MyForm() As UserForm1
    If m_MyForm Is Nothing Then
        Set m_MyForm = New UserForm1
    End If

    Set MyForm = m_MyForm
End Property

然后您可以在整个代码中引用无模式代码,例如

ThisWorkbook.MyForm.Show vbModeless

等等

对于无模式表单,使用 DoEvents 和自定义用户表单 属性。


Sub test()

    Dim frm As New UserForm1

    frm.Show vbModeless

    Do
        DoEvents
        If frm.Cancelled Then
            Unload frm
        Exit Do
    End If
    Loop Until False

    MsgBox "You closed the modeless form."

    '/ Using With
    With New UserForm1
        .Show vbModeless
        Do
            DoEvents
            If .Cancelled Then Exit Do
        Loop Until False
    End With

    MsgBox "You closed the modeless form (with)"

End Sub

'/ 用户表单

Private m_bCancelled As Boolean

Public Property Get Cancelled() As Boolean
    Cancelled = m_bCancelled
End Property

Public Property Let Cancelled(ByVal bNewValue As Boolean)
    m_bCancelled = bNewValue
End Property
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    Me.Cancelled = True
    Cancel = 1
    Me.Hide
End Sub

事实上,我一直非常关注模态形式 - 因为这是最常用的形式。感谢您对该文章的反馈!

非模态形式的原则是相同的:只需扩展链接文章和 中粗略概述的 Model-View-Presenter 模式。

区别在于非模态形式需要范式转变:您不再响应预设的事件序列 - 相反,您需要响应一些异步 事件 可能在任何给定时间发生,也可能不发生。

  • 处理模态表单时,有一个 "before showing",然后是一个 "after hiding",它在隐藏表单后立即运行。您可以使用事件处理 "while showing" 发生的任何事情。
  • 处理非模态形式时,有一个"before showing",然后"while showing"和"after showing" 两个都需要通过事件。

让您的演示者 class 模块负责在模块级别保存 UserForm 实例,并且 WithEvents:

Option Explicit
Private WithEvents myModelessForm As UserForm1

演示者的 Show 方法将 Set 表单实例并显示它:

Public Sub Show()
    'If Not myModelessForm Is Nothing Then
    '    myModelessForm.Visible = True 'just to ensure visibility & honor the .Show call
    '    Exit Sub
    'End If
    Set myModelessForm = New UserForm1
    '...
    myModelessForm.Show vbModeless
End Sub

希望表单实例是此处过程的局部变量,因此局部变量 a With 块无法工作:该对象将在您想要之前超出范围。这就是您将实例存储在模块级别的私有字段中的原因:现在表单与演示者实例一样存在。

现在,您需要向演示者制作表单 "talk" - 最简单的方法是在 UserForm1 代码隐藏中公开事件 - 例如,如果我们希望用户确认取消,我们将向事件添加一个 ByRef 参数,因此演示者中的处理程序可以将信息传递回事件源(即返回表单代码):

Option Explicit
'...private fields, model, etc...
Public Event FormConfirmed()
Public Event FormCancelled(ByRef Cancel as Boolean)

'returns True if cancellation was cancelled by handler
Private Function OnCancel() As Boolean
    Dim cancelCancellation As Boolean
    RaiseEvent FormCancelled(cancelCancellation)
    If Not cancelCancellation Then Me.Hide
    OnCancel = cancelCancellation
End Function

Private Sub CancelButton_Click()
    OnCancel
End Sub

Private Sub OkButton_Click()
    Me.Hide
    RaiseEvent FormConfirmed
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = VbQueryClose.vbFormControlMenu Then
        Cancel = Not OnCancel
    End If
End Sub

现在演示者可以处理 FormCancelled 事件:

Private Sub myModelessForm_FormCancelled(ByRef Cancel As Boolean)
    'setting Cancel to True will leave the form open
    Cancel = MsgBox("Cancel this operation?", vbYesNo + vbExclamation) = vbNo
    If Not Cancel Then
        ' modeless form was cancelled and is now hidden.
        ' ...
        Set myModelessForm = Nothing
    End If
End Sub

Private Sub myModelessForm_FormConfirmed()
    'form was okayed and is now hidden.
    '...
    Set myModelessForm = Nothing
End Sub

非模态表单 通常 没有 "ok" 和 "cancel" 按钮。相反,您会公开许多功能,例如一个会调出一些模态对话框的功能 UserForm2 做其他事情 - 同样,您只需为其公开一个事件,并在演示者中处理它:

Public Event ShowGizmo()

Private Sub ShowGizmoButton_Click()
    RaiseEvent ShowGizmo
End Sub

主持人接着说:

Private Sub myModelessForm_ShowGizmo()
    With New GizmoPresenter
        .Show
    End With
End Sub

请注意,模态 UserForm2 是单独的演示者 class 关注的问题。