【问题标题】:Excel VBA Userform - Execute Sub when something changesExcel VBA 用户窗体 - 发生变化时执行 Sub
【发布时间】:2023-03-27 21:45:02
【问题描述】:

我有一个包含大量文本框的用户表单。当这些文本框的值发生变化时,我需要通过调用子例程 AutoCalc() 根据文本框的值重新计算最终结果值。

我有大约 25 个框,我不想将 Change() 事件单独添加到调用所述子例程的每个文本框。每当某些值发生变化时,调用 AutoCalc() 的最快和最有效的方法是什么?

【问题讨论】:

    标签: vba excel excel-2007 userform


    【解决方案1】:

    查看this,了解如何创建一个响应任何文本框中更改的类。该示例用于按钮,但可以修改。但是,请注意 Textbox 控件没有 Exit 事件(该事件实际上是用户窗体的一部分),因此您确实必须使用 Change 事件。

    【讨论】:

    • 您的链接在上面断开了。
    • @RobertTodar,谢谢,我找到了该页面的新地址。这与 Alex P 接受的答案基本相同。
    【解决方案2】:

    这可以通过使用类模块来实现。在下面的示例中,我将假设您已经有一个带有一些文本框的用户表单。

    首先,在您的 VBA 项目中创建一个类模块(将其命名为 clsTextBox -- 请务必更改类模块的“名称”属性!)

    Private WithEvents MyTextBox As MSForms.TextBox
    
    Public Property Set Control(tb As MSForms.TextBox)
        Set MyTextBox = tb
    End Property
    
    Private Sub MyTextBox_Change()
        AutoCalc() //call your AutoCalc sub / function whenever textbox changes
    End Sub
    

    现在,在用户表单中,添加以下代码:

    Dim tbCollection As Collection
    
    Private Sub UserForm_Initialize()
        Dim ctrl As MSForms.Control
        Dim obj As clsTextBox
    
        Set tbCollection = New Collection
            For Each ctrl In Me.Controls
                If TypeOf ctrl Is MSForms.TextBox Then
                    Set obj = New clsTextBox
                    Set obj.Control = ctrl
                    tbCollection.Add obj
                End If
            Next ctrl
        Set obj = Nothing
    
    End Sub
    

    【讨论】:

    • 嗨,创建 tbCollection 需要什么?它有什么作用?
    • 集合用于存储和持久化创建的文本框对象。
    【解决方案3】:

    类的使用,正如上面的答案所暗示的,它是一种以简洁优雅的方式处理许多控件的好策略,但是:

    1) 我认为用 1 行创建 25 个事件并调用通用用户窗体私有例程没有问题,除非控件的数量是动态的。这是一种KISS 哲学。

    2) 一般来说,我认为 Change 事件非常令人不安,因为他会重新计算每个输入的数字。使用 Exit 事件或 Before Update 事件执行此操作更为明智和适度,因为它仅在确定值时才进行重新计算。例如,Google Instant 在用户未定义问题的情况下试图返回响应,消耗资源,这让我很恼火。

    3) 存在验证问题。我同意您可以通过 Change 事件避免错误的键,但是如果您需要验证数据,您无法知道用户是否会继续输入或数据是否已准备好进行验证。

    4) 您应该记住 Change 或 Exit 事件不会强制用户传入文本字段,因此在尝试更改时需要重新验证和重新计算系统退出表单而不取消。

    下面的代码对静态表单很简单但是很有效。

    Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean)
    Call  AutoCalc(Cancel)
    End Sub
    
    Private Sub TextBox2_Exit(ByVal Cancel As MSForms.ReturnBoolean)
    Call  AutoCalc(Cancel)
    End Sub
    .....
    Private Sub TextBox25_Exit(ByVal Cancel As MSForms.ReturnBoolean)
    Call  AutoCalc(Cancel)
    End Sub
    
    Private Function Valid
    .....
    End Function 
    
    Private Sub AutoCalc(Canc As Variant)
    If Not Valid() Then Canc=True
    '  Calculation
    End Sub
    

    如果您热衷于节省时间,您可以创建一个通用 VBA 例程,以便以适合掩码的形式为与控件相关的事件生成代码。此代码可以在草稿表中(直接生成代码更安全,这在某些 Excel 版本中存在错误),而不是复制并粘贴到表单模块中。

     Sub GenerateEvent(Form As String, Mask As String, _
       Evento As String, Code As String)
     '  Form - Form name in active workbook
     '  Mark - String piece inside control name
     '  Evento - Event name to form procedure name
     '  Code   - Code line inside event
     Dim F As Object
     Dim I As Integer
     Dim L As Long
     Dim R As Range
     Dim Off As Long
     Set F = ThisWorkbook.VBProject.VBComponents(Form)
     Set R = ActiveCell   ' Destination code
     Off = 0
     For I = 0 To F.Designer.Controls.Count - 1
        If F.Designer.Controls(I).Name Like "*" & Mask & "*" Then
            R.Offset(Off, 0) = "Private Sub " & _
              F.Designer.Controls(I).Name & "_" & Evento & "()"
            R.Offset(Off + 1, 0) = "     " & Code
            R.Offset(Off + 2, 0) = "End Sub"
            Off = Off + 4
        End If
     Next I
     End Sub
    
     Sub Test()
     Call GenerateEvent("FServCons", "tDt", "Exit", _
        "Call AtuaCalc(Cancel)")
     End Sub
    

    【讨论】:

      【解决方案4】:

      但是,请注意,文本框控件没有 Exit 事件(该事件实际上是用户窗体的一部分),因此您确实必须使用 Change 事件。

      我很困惑。也许这是在 2007 年添加的,或者我不明白其中的细微差别。我在 TextBox 控件上使用 Exit 事件。当我 Tab 退出控件,或者在另一个控件上单击鼠标时,它会触发 Exit 事件。

      【讨论】:

        【解决方案5】:

        我有一个类似的问题,我想使用一个通用例程验证大约 48 个不同的文本框,并且类模块方法看起来很有趣(重复的代码行要少得多)。但我不想验证输入的每个字符,我只想在更新后检查。如果输入的数据无效,我想清除文本框并留在同一个文本框中,这需要在退出例程中使用 Cancel = True。经过几个小时的尝试并且没有让我的 AfterUpdate 和 Exit 事件处理程序从未触发我发现了原因。

        如果你创建一个像下面这样的类:

        Private WithEvents MyTextBox As MSForms.TextBox
        
        Public Property Set** Control(tb As MSForms.TextBox)
        
            Set MyTextBox = tb
        
        End Property
        

        然后你进入VBE对象浏览器并选择MyTextBox,你会看到支持的枚举事件不包括AfterUpdate或Exit。如果您进入 UserForm 并使用 VBE 对象浏览器并查看 TextBox 的实例,则这些事件可用,但它们似乎是从 TextBox 所属的控件继承的。使用 MSForms.TextBox 定义新类不包括这些事件。如果您尝试手动定义这些事件处理程序,它们将编译并且看起来它们会工作(但它们不会)。它们不会成为类对象的事件处理程序,而只是显示在 VBE 对象浏览器下的 (General) 中的私有子例程,并且永远不会被执行。看来创建有效事件处理程序的唯一方法是在 VBE 对象浏览器中选择类对象,然后从枚举事件列表中选择所需的事件。

        经过数小时的搜索,我无法找到任何参考资料来说明如何在私有类中构建类似的继承模型,因此 AfterUpdate 和 Exit 将显示为已创建类的可用事件。因此,如果您想使用 AfterUpdate 和/或 Exit,建议(上面)为 UserForm 上的每个 TextBox 设置一个单独的事件处理程序可能是唯一可行的方法。

        【讨论】:

        • 非常有趣的观察 - 我想避免这个问题的方法是永远不要手动输入事件处理程序,而是使用编辑器右上角的下拉列表来获取所有可能的列表事件(因此您永远不会尝试添加不受支持的事件)。至于解决方案,只需声明WithEvents MyTextBox As MSForms.Control(即从文本框向下转换为通用控件),您应该可以访问您所关注的事件。没想到扩展类也不继承事件,很奇怪
        • Ps,你是对的,这种继承模型不能用纯 VBA 复制
        【解决方案6】:

        所以前 9 行是在论坛中给我的,我不记得在哪里。但我以此为基础,现在我想使用命令按钮来重新计算是否使用更改了此子项中列出的变量。

        Private Sub txtWorked_Exit(ByVal Cancel As MSForms.ReturnBoolean)
            11 Dim OTRate       As Double
               OTRate = Me.txtHourlyRate * 1.5
            If Me.txtWorked > 40 Then
               Me.txtBasePay.Value = Format(Me.txtHourlyRate.Value * 40, "$#,##0.00")
               Me.txtOvertime = Format((Me.txtWorked - 40) * OTRate, "$#,##0.00")
            Else
               Me.txtOvertime.Value = "0"
               Me.txtBasePay.Value = Format(Me.txtHourlyRate.Value * Me.txtWorked.Value, "$#,##0.00")
            End If
            Dim Gross, W2, MASSTax, FICA, Medi, Total, Depends, Feds As Double
               Gross = CDbl(txtBonus.Value) + CDbl(txtBasePay.Value) +    CDbl(txtOvertime.Value)
               W2 = txtClaim * 19
               Me.txtGrossPay.Value = Format(Gross, "$#,##0.00")
               FICA = Gross * 0.062
               Me.txtFICA.Value = Format(FICA, "$#,##0.00")
               Medi = Gross * 0.0145
               Me.txtMedicare.Value = Format(Medi, "$#,##0.00")
               MASSTax = (Gross - (FICA + Medi) - (W2 + 66)) * 0.0545
            If chkMassTax = True Then
               Me.txtMATax.Value = Format(MASSTax, "$#,##0.00")
            Else: Me.txtMATax.Value = "0.00"
            End If
            If Me.txtClaim.Value = 1 Then
               Depends = 76.8
        
            ElseIf Me.txtClaim.Value = 2 Then
               Depends = 153.8
        
            ElseIf Me.txtClaim.Value = 3 Then
               Depends = 230.7
            Else
               Depends = 0
            End If
               If (Gross - Depends) < 765 Then
               Feds = ((((Gross - Depends) - 222) * 0.15) + 17.8)
               Me.txtFedIncome.Value = Format(Feds, "$#,##.00")
            ElseIf (Gross - Depends) > 764 Then
               Feds = ((((Gross - Depends) - 764) * 0.25) + 99.1)
               Me.txtFedIncome.Value = Format(Feds, "$#,##.00")
            Else:
               Feds = 0
            End If
               Total = (txtMATax) + (FICA) + (Medi) + (txtAdditional) + (Feds)
               Me.txtTotal.Value = Format(Total, "$#,##0.00")
               Me.txtNetPay.Value = Format(Gross - Total, "$#,##0.00")
        
        End Sub
        
        Private Sub cmdReCalculate_Click()
        
        End Sub
        

        【讨论】:

          【解决方案7】:

          二类模块方法

          我创建了一种非常简单的方法来将事件侦听器添加到用户窗体。此外,它还添加了诸如 MouseOver 和 MouseOut 之类的事件。 (做悬停效果很酷)

          需要导入才能工作的两个类模块可以在我的Github页面VBA Userform Event Listeners找到


          如何在用户窗体中使用代码

          很容易上手,一旦你添加了我的类模块,只需将下面的示例代码添加到用户窗体中。

          Private WithEvents Emitter As EventListnerEmitter
              
          Private Sub UserForm_Activate()
             Set Emitter = New EventListnerEmitter
             Emitter.AddEventListnerAll Me
          End Sub
          

          就是这样! 现在您可以开始监听不同的事件了。


          主事件监听器

          有主要事件EmittedEvent。这将传入事件所在的控件和事件名称。所以所有的事件都会经过这个事件处理程序。

          Private Sub Emitter_EmittedEvent(Control As Object, ByVal EventName As String, EventValue As Variant)
              
              If TypeName(Control) = "Textbox" And EventName = "Change" Then
                  'DO WHATEVER
              End If
            
          End Sub
          

          单个事件监听器

          您也可以只监听特定事件。 o 在这种情况下是更改事件。

          Private Sub Emitter_Change(Control As Object)
          
              If TypeName(Control) = "Textbox" Then
                    'DO WHATEVER
              End If
              
          End Sub
          

          请随时查看我的Github 页面并提出拉取请求,因为尚未捕获所有事件。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-02-09
            • 1970-01-01
            • 2018-11-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多