【问题标题】:VBA - destroy a modeless UserForm instance properlyVBA - 正确销毁无模式的用户窗体实例
【发布时间】:2018-05-01 15:31:59
【问题描述】:

简介:

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

  • 在用户表单代码中处理QueryClose (If CloseMode = vbFormControlMenu ...)
  • 在其中不做Unload Me,只是一个胆小的Me.Hide指令 (在通过Cancel = True 防止 [x]-itting 和最终自毁之后)
  • 在 [class] 代码中设置相关变量/[property](例如.IsCancelled=True
  • 为了能够通过调用代码卸载UF。

有用的链接

一个出色的概述“UserForm1.Show?”可以在https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/找到 以及许多示例性 SO 答案(感谢 Mathieu Guindon aka 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

如果我尝试,但是

  • a) 获取有关可能的用户取消的信息以及
  • b) 到Unload 的一个表格,如果在Show 指令之后使用局部变量(例如“ufA”)受洗,

由于表单是 MODELESS,所有代码行都将立即执行:

  • 代码显示表单,下一刻..
  • 代码发现没有用户取消,因为没有时间进行任何用户操作,下一刻..
  • [如果对用户表单使用局部变量,代码将卸载表单]

3) 问题

我该如何处理 a) 通过 MODELESS 表单的调用代码正确报告的 UserForm 取消以及 b) 如果使用局部变量则(必要?)卸载?

【问题讨论】:

  • @Mat's Mug ad) 编辑:当然在 1a) 中必须有一个 If Not **Ufa**.IsCancelled - 人们应该避免在下面的一些行中复制一个明显相似的代码(参见 1b):-)。尽管如此,我希望至少为学习程序员提供了一个有用的问题。
  • @Mat's Mug,感谢您通过评论澄清了一些背景,从而证实了我探索 VBA 的 OOP 能力并尝试 MVP 解决方案的决心。如果仅是由于不同的哲学而导致的(无法解释的)投票,那就更不公平了。顺便说一句,橡胶可以是一种非常坚固的材料:-;
  • 我什至会说SOLID

标签: vba excel userform


【解决方案1】:

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

不过,非模态表单的原则是相同的:只需扩展链接文章和here 中大致概述的 Model-View-Presenter 模式即可。

不同之处在于非模态表单需要进行范式转换:您不再响应预设的事件序列 - 相反,您需要响应一些可能发生的异步事件在任何给定的时间,或者不是。

  • 在处理模态表单时,有一个“显示前”和一个“隐藏后”,在表单隐藏后立即运行。您可以使用事件处理“显示时”发生的任何事情。
  • 在处理非模态表单时,有“显示前”、“显示中”和“显示后”需要通过事件来处理。

让您的演示者类模块负责在模块级别和WithEvents 上保存UserForm 实例:

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

希望表单实例在此处的过程中是本地的,因此局部变量 With 块不起作用:对象将超出范围之前你的意思。这就是您将实例存储在模块级别的私有字段中的原因:现在表单的存在时间与演示者实例一样长。

现在,您需要让表单与演示者“交谈” - 最简单的方法是在 UserForm1 代码隐藏中公开事件 - 例如,如果我们希望用户确认取消,我们将添加一个ByRef参数给事件,所以presenter中的handler可以将信息传回事件源(即传回表单代码):

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

非模态表单通常不会有“确定”和“取消”按钮。相反,您会公开许多功能,例如,一个会调出一些模态对话框 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 是单独的演示者类的关注点。

【讨论】:

  • 感谢您提供有用且详细的描述,不胜感激。
  • 这是一个非常翔实的答案和很棒的知识。它只是一个过度杀伤。如果您需要在 VBA 项目中创建/部署这种类型的解决方案,那么您肯定没有使用最适合您的工作的工具。 VBA 可以做很多事情。但仅仅因为它可以,并不意味着你应该。尝试在 VBA 中实现 MVVM 或 MVC。可能的?是的,正如@Mat'sMug 非常正确地展示的那样。但是,如果您需要 MVC 实现并且由于任何原因仍然坚持使用 Excel-VBA,请重新考虑您的规范并重新分析。同样,这个答案非常好。
  • 当我在 Word 中尝试此操作时,似乎没有触发事件。但是当我将表单显示为vbModal 时,所有事件都会触发并得到处理。有没有办法获得一个无模式的表单来引发 Word 中的事件?
  • @xidgel 奇怪,MSForms.UserForm.Show 的可选参数的默认值是vbModal... 你不能声明一个本地表单,你在同一个过程范围内显示无模式 - 执行不会在.Show 处被阻止,并且表单几乎会立即超出范围 - 您需要在模块级别保存该对象引用,以便该对象仍然可以通过编程方式访问。
  • 我将表单对象设为演示者类的私有实例字段,并在标准模块中的模块级别拥有演示者实例。
【解决方案2】:

我通常将无模式用户窗体实例的生命周期与工作簿的生命周期联系起来,方法是将代码放在 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

等等

【讨论】:

  • 另一种工作方法,可能存在延迟卸载的缺点。
  • 我想复制您的解决方案,但这似乎对我不起作用。我无法访问 ThisWoorkBook.MyForm 属性。
  • 您的工作簿对象的名称确实是“ThisWorkbook”吗?可以在 VBA 编辑器中,在工作簿的属性列表中的 (Name) 属性下更改此名称。例如,如果工作簿的(名称)是 MyWorkbook,您将调用 MyWorkbook.MyForm.Show。另外,请仔细检查您是否使用了朋友访问级别;私人不行。如果它仍然不起作用,请告诉我更多有关您尝试访问该属性的上下文。
【解决方案3】:

对于无模式表单,使用 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

【讨论】:

  • 认为这是一个优雅的方法,让调用代码负责销毁/卸载表单。
  • 添加一个while循环来测试表单是否关闭可能还不如做一个模态表单。您将暂停执行,直到表单关闭。
猜你喜欢
  • 2011-04-06
  • 1970-01-01
  • 2012-05-16
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
  • 2015-03-08
  • 2016-11-13
  • 1970-01-01
相关资源
最近更新 更多