如何避免在Excel VBA中使用Select

How to avoid using Select in Excel VBA

我听说过很多关于在Excel VBA中使用.Select的可理解的憎恶,但我不确定如何避免使用它。 我发现如果我能够使用变量而不是Select函数,我的代码将更加可重用。 但是,如果不使用Select,我不确定如何引用(如ActiveCell等)。

我发现这篇关于范围的文章和这个例子关于不使用select的好处却找不到怎么样的东西?


一些如何避免选择的例子

使用Dim'd变量

1
Dim rng as Range

Set变量到所需范围。有很多方法可以指代单细胞范围

1
2
3
Set rng = Range("A1")
Set rng = Cells(1,1)
Set rng = Range("NamedRange")

或多细胞范围

1
2
3
4
5
Set rng = Range("A1:B10")
Set rng = Range("A1","B10")
Set rng = Range(Cells(1,1), Cells(10,2))
Set rng = Range("AnotherNamedRange")
Set rng = Range("A1").Resize(10,2)

您可以使用Evaluate方法的快捷方式,但效率较低,通常应避免在生产代码中使用。

1
2
Set rng = [A1]
Set rng = [A1:B10]

以上所有示例均指活动纸上的单元格。除非您特别想要仅使用活动工作表,否则最好还要使用Worksheet变量

1
2
3
4
5
6
Dim ws As Worksheet
Set ws = Worksheets("Sheet1")
Set rng = ws.Cells(1,1)
With ws
    Set rng = .Range(.Cells(1,1), .Cells(2,10))
End With

如果您确实想使用ActiveSheet,为清楚起见,最好是明确的。但请注意,因为一些Worksheet方法会更改活动工作表。

1
Set rng = ActiveSheet.Range("A1")

同样,这指的是活动工作簿。除非您特别想要仅使用ActiveWorkbookThisWorkbook,否则最好还要对Workbook变量进行调暗。

1
2
3
Dim wb As Workbook
Set wb = Application.Workbooks("Book1")
Set rng = wb.Worksheets("Sheet1").Range("A1")

如果您确实想使用ActiveWorkbook,为了清楚起见,最好是明确的。但请注意,因为许多Workbook方法会更改活动的书籍。

1
Set rng = ActiveWorkbook.Worksheets("Sheet1").Range("A1")

您还可以使用ThisWorkbook对象来引用包含正在运行的代码的书籍。

1
Set rng = ThisWorkbook.Worksheets("Sheet1").Range("A1")

一个常见的(坏)代码是打开一本书,获取一些数据然后再关闭

这是不好的:

1
2
3
4
5
6
7
8
9
10
Sub foo()
    Dim v as Variant
    Workbooks("Book1.xlsx").Sheets(1).Range("A1").Clear
    Workbooks.Open("C:\Path\To\SomeClosedBook.xlsx")
    v = ActiveWorkbook.Sheets(1).Range("A1").Value
    Workbooks("SomeAlreadyOpenBook.xlsx").Activate
    ActiveWorkbook.Sheets("SomeSheet").Range("A1").Value = v
    Workbooks(2).Activate
    ActiveWorkbook.Close()
End Sub

并且会更好:

1
2
3
4
5
6
7
8
9
10
Sub foo()
    Dim v as Variant
    Dim wb1 as Workbook
    Dim  wb2 as Workbook
    Set wb1 = Workbooks("SomeAlreadyOpenBook.xlsx")
    Set wb2 = Workbooks.Open("C:\Path\To\SomeClosedBook.xlsx")
    v = wb2.Sheets("SomeSheet").Range("A1").Value
    wb1.Sheets("SomeOtherSheet").Range("A1").Value = v
    wb2.Close()
End Sub

将范围传递到SubFunction作为范围变量

1
2
3
4
5
6
7
8
9
10
Sub ClearRange(r as Range)
    r.ClearContents
    '....
End Sub

Sub MyMacro()
    Dim rng as Range
    Set rng = ThisWorkbook.Worksheets("SomeSheet").Range("A1:B10")
    ClearRange rng
End Sub

您还应该将方法(例如FindCopy)应用于变量

1
2
3
4
5
Dim rng1 As Range
Dim rng2 As Range
Set rng1 = ThisWorkbook.Worksheets("SomeSheet").Range("A1:A10")
Set rng2 = ThisWorkbook.Worksheets("SomeSheet").Range("B1:B10")
rng1.Copy rng2

如果您在一系列单元格上循环,通常更好(更快)将范围值首先复制到变量数组并循环遍历

1
2
3
4
5
6
7
8
9
10
Dim dat As Variant
Dim rng As Range
Dim i As Long

Set rng = ThisWorkbook.Worksheets("SomeSheet").Range("A1:A10000")
dat = rng.Value  ' dat is now array (1 to 10000, 1 to 1)
for i = LBound(dat, 1) to UBound(dat, 1)
    dat(i,1) = dat(i,1) * 10 'or whatever operation you need to perform
next
rng.Value = dat ' put new values back on sheet

尽管如此,这是一个很小的品尝者。


应该避免.Select / .Activate / Selection / Activecell / ActiveSheet / ActiveWorkbook等的两个主要原因

  • 它会减慢你的代码速度。
  • 它通常是运行时错误的主要原因。
  • 我们如何避免它?

    1)直接使用相关对象

    考虑这段代码

    1
    2
    3
    4
    Sheets("Sheet1").Activate
    Range("A1").Select
    Selection.Value ="Blah"
    Selection.NumberFormat ="@"

    此代码也可以写为

    1
    2
    3
    4
    With Sheets("Sheet1").Range("A1")
        .Value ="Blah"
        .NumberFormat ="@"
    End With

    2)如果需要声明你的变量。上面的代码可以写成

    1
    2
    3
    4
    5
    6
    7
    8
    Dim ws as worksheet

    Set ws = Sheets("Sheet1")

    With ws.Range("A1")
        .Value ="Blah"
        .NumberFormat ="@"
    End With


    我将在上面给出的所有优秀答案中加上一个小重点:

    您可以做的最大的事情是避免使用Select,尽可能在VBA代码中使用命名范围(结合有意义的变量名称)。上面提到了这一点,但有点掩饰;但是,它值得特别关注。

    以下是使用命名范围的另外一些理由,但我确信我可以想到更多。

    命名范围使您的代码更易于阅读和理解。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Dim Months As Range
    Dim MonthlySales As Range

    Set Months = Range("Months")
    'e.g,"Months" might be a named range referring to A1:A12

    Set MonthlySales = Range("MonthlySales")
    'e.g,"Monthly Sales" might be a named range referring to B1:B12

    Dim Month As Range
    For Each Month in Months
        Debug.Print MonthlySales(Month.Row)
    Next Month

    命名区域MonthsMonthlySales包含的内容以及过程正在执行的操作非常明显。

    为什么这很重要?部分是因为其他人更容易理解它,但即使你是唯一一个会看到或使用你的代码的人,你仍然应该使用命名范围和良好的变量名,因为你会忘记你的意图一年之后,你将浪费30分钟来弄清楚你的代码在做什么。

    命名范围可确保在电子表格的配置发生更改时(而不是!),您的宏不会中断。

    考虑一下,如果上面的例子是这样编写的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Dim rng1 As Range
    Dim rng2 As Range

    Set rng1 = Range("A1:A12")
    Set rng2 = Range("B1:B12")

    Dim rng3 As Range
    For Each rng3 in rng1
        Debug.Print rng2(rng3.Row)
    Next rng3

    这个代码最初会正常运行 - 直到你或未来的用户决定"gee wiz,我想我要在Column A中添加一个新列的年份!",或者在月份和销售列,或为每列添加标题。现在,您的代码已损坏。而且因为你使用了可怕的变量名,所以你需要花费更多的时间来弄清楚它应该如何修复它。

    如果您已经开始使用命名范围,那么MonthsSales列可以随心所欲地移动,并且您的代码将继续正常工作。


    我会给出简短的答案,因为其他人都给了很长的答案。

    每当你录制宏并重用它们时,你都会得到.select和.activate。当你选择一个单元格或工作表时,它就会激活它。从那时起,无论何时使用非限定引用(如Range.Value),它们都只使用活动单元格和工作表。如果您不查看代码放置位置或用户单击工作簿,这也可能会出现问题。

    因此,您可以通过直接引用您的单元格来消除这些问题。哪个:

    1
    2
    3
    4
    5
    'create and set a range
    Dim Rng As Excel.Range
    Set Rng = Workbooks("Book1").Worksheets("Sheet1").Range("A1")
    'OR
    Set Rng = Workbooks(1).Worksheets(1).Cells(1, 1)

    或者你可以

    1
    2
    3
    4
    5
    'Just deal with the cell directly rather than creating a range
    'I want to put the string"Hello" in Range A1 of sheet 1
    Workbooks("Book1").Worksheets("Sheet1").Range("A1").value ="Hello"
    'OR
    Workbooks(1).Worksheets(1).Cells(1, 1).value ="Hello"

    这些方法有各种各样的组合,但对于像我这样的不耐烦的人,这将是尽快表达的一般想法。


    "... and am finding that my code would be more re-usable if I were able to use variables instead of Select functions."

    虽然我不能想到的只是一些孤立的情况,其中.Select比直接的单元格引用更好的选择,我会提升Selection的防御并指出它不应该被抛弃应该避免.Select的原因。

    有时候,通过点击几个键,分配给热键组合的短节省时间的宏子程序可以节省大量时间。当处理不符合工作表范围数据格式的袋装数据时,能够选择一组单元格来制定操作代码。与选择一组单元格并应用格式更改的方式大致相同,选择一组单元格来运行特殊宏代码可以节省大量时间。

    基于选择的子框架示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    Public Sub Run_on_Selected()
        Dim rng As Range, rSEL As Range
        Set rSEL = Selection    'store the current selection in case it changes
        For Each rng In rSEL
            Debug.Print rng.Address(0, 0)
            'cell-by-cell operational code here
        Next rng
        Set rSEL = Nothing
    End Sub

    Public Sub Run_on_Selected_Visible()
        'this is better for selected ranges on filtered data or containing hidden rows/columns
        Dim rng As Range, rSEL As Range
        Set rSEL = Selection    'store the current selection in case it changes
        For Each rng In rSEL.SpecialCells(xlCellTypeVisible)
            Debug.Print rng.Address(0, 0)
            'cell-by-cell operational code here
        Next rng
        Set rSEL = Nothing
    End Sub

    Public Sub Run_on_Discontiguous_Area()
        'this is better for selected ranges of discontiguous areas
        Dim ara As Range, rng As Range, rSEL As Range
        Set rSEL = Selection    'store the current selection in case it changes
        For Each ara In rSEL.Areas
            Debug.Print ara.Address(0, 0)
            'cell group operational code here
            For Each rng In ara.Areas
                Debug.Print rng.Address(0, 0)
                'cell-by-cell operational code here
            Next rng
        Next ara
        Set rSEL = Nothing
    End Sub

    要处理的实际代码可以是从单行到多个模块的任何内容。我已经使用这种方法在包含外部工作簿文件名的不规则选择的单元格上启动长时间运行的例程。

    简而言之,由于与.SelectActiveCell的紧密关联,请不要丢弃Selection。作为工作表属性,它有许多其他用途。

    (是的,我知道这个问题是关于.Select,而不是Selection但我想删除新手VBA编码员可能推断的任何误解。)


    请注意,在下面我将比较Select方法(OP希望避免的方法)和Range方法(这是问题的答案)。因此,当您看到第一个选择时,请不要停止阅读。

    这实际上取决于你想要做什么。无论如何,一个简单的例子可能很有用。我们假设您要将活动单元格的值设置为"foo"。使用ActiveCell你会写这样的东西:

    1
    2
    3
    Sub Macro1()
        ActiveCell.Value ="foo"
    End Sub

    如果要将其用于非活动单元格,例如"B2",则应首先选择它,如下所示:

    1
    2
    3
    4
    Sub Macro2()
        Range("B2").Select
        Macro1
    End Sub

    使用范围,您可以编写一个更通用的宏,可用于设置任何您想要的单元格的值:

    1
    2
    3
    Sub SetValue(cellAddress As String, aVal As Variant)
        Range(cellAddress).Value = aVal
    End Sub

    然后,您可以将Macro2重写为:

    1
    2
    3
    Sub Macro2()
        SetCellValue"B2","foo"
    End Sub

    和Macro1一样:

    1
    2
    3
    Sub Macro1()
        SetValue ActiveCell.Address,"foo"
    End Sub

    希望这有助于清理一点点。


    避免SelectActivate是让VBA开发人员更好的举措。通常,在记录宏时使用SelectActivate,因此Parent工作表或范围始终被视为活动的。

    这是在以下情况下可以避免SelectActivate的方法:

    添加新工作表并在其上复制单元格:

    从(使用宏录制器生成的代码):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Sub Makro2()
        Range("B2").Select
        Sheets.Add After:=ActiveSheet
        Sheets("Tabelle1").Select
        Sheets("Tabelle1").Name ="NewName"
        ActiveCell.FormulaR1C1 ="12"
        Range("B2").Select
        Selection.Copy
        Range("B3").Select
        ActiveSheet.Paste
        Application.CutCopyMode = False
    End Sub

    至:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Sub TestMe()
        Dim ws As Worksheet
        Set ws = Worksheets.Add
        With ws
            .Name ="NewName"
            .Range("B2") = 12
            .Range("B2").Copy Destination:=.Range("B3")
        End With
    End Sub

    如果要在工作表之间复制范围:

    从:

    1
    2
    3
    4
    5
    6
    Sheets("Source").Select
    Columns("A:D").Select
    Selection.Copy
    Sheets("Target").Select
    Columns("A:D").Select
    ActiveSheet.Paste

    至:

    1
    Worksheets("Source").Columns("A:D").Copy Destination:=Worksheets("Target").Range("a1")

    使用花哨的命名范围

    您可以使用[]访问它们。与其他方式相比,这真的很美。自行检查:

    1
    2
    3
    4
    5
    6
    7
    8
    Dim Months As Range
    Dim MonthlySales As Range

    Set Months = Range("Months")    
    Set MonthlySales = Range("MonthlySales")

    Set Months =[Months]
    Set MonthlySales = [MonthlySales]

    上面的例子如下所示:

    1
    Worksheets("Source").Columns("A:D").Copy Destination:=Worksheets("Target").[A1]

    不是复制值,而是采用它们

    通常,如果你愿意Select,很可能你正在复制一些东西。如果您只对值感兴趣,这是一个避免选择的好选择:

    Range("B1:B6").Value = Range("A1:A6").Value

    请始终尝试引用工作表

    这可能是vba最常见的错误。无论何时复制范围,有时都不会引用工作表,因此VBA会考虑ActiveWorksheet。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    'This will work only if the 2. Worksheet is selected!
    Public Sub TestMe()
        Dim rng As Range
        Set rng = Worksheets(2).Range(Cells(1, 1), Cells(2, 2)).Copy
    End Sub

    'This works always!
    Public Sub TestMe2()
        Dim rng As Range
        With Worksheets(2)
            .Range(.Cells(1, 1), .Cells(2, 2)).Copy
        End With
    End Sub

    我真的不能使用.Select.Activate吗?

    唯一可以证明您使用.Activate.Select的时间是确定的,因为视觉原因选择了特定的工作表。例如,您的Excel将始终打开,首先选择封面工作表,忽略文件关闭时哪个是活动表。因此,这样的事情是绝对可以的:

    1
    2
    3
    Private Sub Workbook_Open()
        Worksheets("Cover").Activate
    End Sub

    始终说明工作簿,工作表和单元格/范围。

    例如:

    1
    2
    Thisworkbook.Worksheets("fred").cells(1,1)
    Workbooks("bob").Worksheets("fred").cells(1,1)

    因为最终用户总是只需单击按钮,一旦焦点移出代码想要使用的工作簿,那么事情就完全错了。

    永远不要使用工作簿的索引。

    1
    Workbooks(1).Worksheets("fred").cells(1,1)

    当用户运行代码时,您不知道将打开哪些其他工作簿。


    这些方法相当耻辱,所以带头@Vityata和@Jeeped是为了在沙子中画一条线:

    为什么不调用.Activate.SelectSelectionActiveSomething方法/属性

    基本上是因为它们主要用于通过应用程序UI处理用户输入。由于它们是用户通过UI处理对象时调用的方法,因此它们是宏录制器记录的方法,这就是为什么在大多数情况下调用它们要么脆弱要么多余:你不必选择一个对象,以便在之后使用Selection执行操作。

    但是,这个定义解决了它们所要求的情况:

    何时调用.Activate.Select.Selection.ActiveSomething方法/属性

    基本上,当您希望最终用户在执行中发挥作用时。

    如果您正在开发并希望用户选择要处理的代码的对象实例,那么.Selection.ActiveObject是合适的。

    另一方面,当您可以推断用户的下一个操作并希望您的代码引导用户时,.Select.Activate是有用的,可能会节省他一些时间和鼠标点击。例如,如果您的代码刚刚创建了一个全新的图表实例或更新过的图表,那么用户可能需要将其检出,您可以在其上或其工作表上调用.Activate以节省用户搜索它的时间;或者如果您知道用户需要更新某些范围值,您可以以编程方式选择该范围。


    恕我直言,.Select的使用来自于那些喜欢我的人开始通过录制宏开始学习VBA然后修改代码而没有意识到.Select和后续selection只是一个不必要的中间人。

    通过直接使用已经存在的对象,可以避免已经发布的.Select,这允许各种间接引用,例如以复杂的方式计算i和j,然后编辑单元格(i,j)等。

    否则,.Select本身没有任何隐含的错误,您可以轻松地找到它的用途,例如我有一个电子表格,我用日期填充,激活宏,用它做一些魔术,并以可接受的格式将其导出到一个单独的工作表上,然而,这需要一些最终的手动(不可预测)输入到相邻的单元格。所以.Select的时刻就为我节省了额外的鼠标移动和点击。


    快速回答:

    要避免使用.Select方法,可以设置一个等于所需属性的变量。

    ?例如,如果您想要Cell A1中的值,则可以将变量设置为等于该单元格的value属性。

    • 示例valOne = Range("A1").Value

    ?例如,如果您想要"Sheet3"的代号,则可以设置一个等于该工作表的代码名称属性的变量。

    • 示例valTwo = Sheets("Sheet3").Codename

    我希望有所帮助。如果您有任何疑问,请告诉我。


    我注意到这些答案都没有提到.Offset属性。这也可用于避免在操作某些单元格时使用Select操作,特别是在引用所选单元格时(如OP提到ActiveCell)。

    这里有几个例子。

    我还假设"ActiveCell"是J4。

    ActiveCell.Offset(2, 0).Value = 12

    • 这会将单元格J6更改为值12
    • 减-2会引用J2

    ActiveCell.Offset(0,1).Copy ActiveCell.Offset(,2)

    • 这会将k4中的单元格复制到L4
    • 注意,如果不需要,偏移参数中不需要"0"(,2)
    • 与前一个示例类似,减1将是i4

    ActiveCell.Offset(, -1).EntireColumn.ClearContents

    • 这将清除列k中所有单元格中的值。

    这并不是说它们比上述选择"更好",而只是列出替代方案。


    这是一个清除单元格"A1"的内容的示例(如果选择类型是xllastcell等,则更多)。全部完成而无需选择细胞。

    1
    2
    Application.GoTo Reference:=Workbook(WorkbookName).Worksheets(WorksheetName).Range("A1")
    Range(Selection,selection(selectiontype)).clearcontents

    我希望这可以帮助别人。