通过VBA Excel分页符

时间:2009-06-12 12:55:57

标签: excel vba page-break

作为报告生成器大修的一部分,我看到了我认为效率低下的代码。这部分代码在生成主报告后运行,以在逻辑位置设置分页符。标准是:

  • 每个网站都在新页面上开始。
  • 不允许群组跨页。

代码遵循以上格式:2个循环执行这些工作。

这是原始代码(抱歉长度):

Public Sub PageBreak(ByRef wstWorksheet As Excel.Worksheet, ByVal pctProgress As ProgressCtl.ProgressControl)
Dim breaksMoved As Integer
Dim p As HPageBreak
Dim i As Integer

'Used as a control value
breaksMoved = 1

' Marks that no rows/columns are to be repeated on each page
wstWorksheet.Activate
wstWorksheet.PageSetup.PrintTitleRows = ""
wstWorksheet.PageSetup.PrintTitleColumns = ""

'If this isn't performed beforehand, then the HPageBreaks object isn't available
Range("A3").Select
ActiveWindow.View = xlPageBreakPreview

'Defaults the print area to be the entire sheet
wstWorksheet.DisplayPageBreaks = False
wstWorksheet.PageSetup.PrintArea = ""

Range("$B$4").Select

' add breaks after each site
Do While ActiveCell.Row <= wstWorksheet.UsedRange.Rows.Count
    If ActiveCell.FormulaR1C1 = "Site ID" Then
        ActiveCell.PageBreak = xlPageBreakManual
    End If
    ActiveCell.Offset(1, 0).Activate
    pctProgress.ProgressText = "Row " & CStr(ActiveCell.Row) & " of " & CStr(wstWorksheet.UsedRange.Rows.Count)
Loop

Dim passes As Long
Do While breaksMoved = 1
    passes = passes + 1
    breaksMoved = 0
    For i = 1 To wstWorksheet.HPageBreaks.Count - 1
            Set p = wstWorksheet.HPageBreaks.Item(i)
            'Selects the first page break
            Range(p.Location.Address).Select
            'Sets the ActiveCell to 1 row above the page break
            ActiveCell.Offset(-1, 0).Activate

            'Move the intended break point up to the first blank section
            Do While Not ActiveCell.FormulaR1C1 = ""
                ActiveCell.Offset(-1, 0).Activate
                breaksMoved = 1
            Loop

            'Add the page break
            If ActiveCell.FormulaR1C1 <> "Site ID" Then
                ActiveCell.Offset(1, 0).Activate
                wstWorksheet.HPageBreaks.Add ActiveCell
            End If

            pctProgress.ProgressText = "Set break point " & CStr(passes) & "." & CStr(i)

    Next

Loop

'Reset the view to normal
wstWorksheet.DisplayPageBreaks = True
ActiveWindow.View = xlNormalView
Range("A3").Select
End Sub

看到改进的空间我着手修改这个。作为新要求之一,人们想要报告是在打印之前手动删除页面。所以我在另一个页面上添加了复选框并复制了选中的项目。为了方便我使用命名范围。我使用这些命名范围来满足第一个要求:

' add breaks after each site   
For Each RangeName In ActiveWorkbook.Names
    If Mid(RangeName.Name, 1, 1) = "P" Then
        Range(RangeName).Activate
        ActiveCell.Offset(Range(RangeName).Rows.Count - 1, 0).Select
        ActiveCell.PageBreak = xlPageBreakManual
    End If
Next RangeName

所有范围都以P_为前缀(对于父级)。使用蹩脚的Now()风格的粗略时间,这在我的短4站点报告和更具挑战性的15站点报告上慢了1秒。它们分别有606和1600行。

1秒并不是那么糟糕。让我们看看下一个标准。 每个逻辑组都由一个空行拆分,因此最简单的方法是找到下一个分页符,然后返回,直到找到下一个空白行并插入新的分隔符。冲洗并重复。

那为什么原版会经历多次?我们也可以改进它(循环外的锅炉板是相同的)。

Dim i As Long
Dim oPageBreak As HPageBreak
Do While i < shtDeliveryVariance.HPageBreaks.Count - 1
    i = i + 1
    pctProgress.ProgressText = "Setting Page Break " & CStr(i) & " of " & CStr(shtDeliveryVariance.HPageBreaks.Count)

    Set oPageBreak = shtDeliveryVariance.HPageBreaks.Item(i)

    ' select the page break
    Range(oPageBreak.Location.Address).Select
    ActiveCell.Offset(-1, 0).Activate

    ' move up to a free row
    Do While Not ActiveCell.FormulaR1C1 = ""
        ActiveCell.Offset(-1, 0).Activate
    Loop

    'Add the page break
    If ActiveCell.FormulaR1C1 <> "Site ID" Then
        ActiveCell.Offset(1, 0).Activate
        shtDeliveryVariance.HPageBreaks.Add ActiveCell
    End If

Loop

一次通过也更优雅。但它快多少?在较小的测试中,与原始的45秒相比需要54秒,而在较大的测试中,我的代码在153到130秒时再次变慢。这也是3次运行的平均值。

所以我的问题是:为什么我的新代码比原版慢得多,尽管我看起来更快我该怎么做以加快代码的缓慢

注意:Screen.Updating等已经关闭,计算等已经关闭。

3 个答案:

答案 0 :(得分:13)

我认为代码中的几个位置有改进的余地:

  1. 不要访问缓慢执行的属性,例如usedrange.rows.count多次(特别是在循环内),除非您认为它们可能有更改。而是将它们存储在变量中。
  2. 如果可以避免文本比较,请不要进行文本比较(例如:.Value =“”),而是使用LenB函数检查空白,它会更快地执行,因为它只是读取字符串头的长度而不是通过字节串比较启动到字节。 (您可能会喜欢this阅读。)
  3. 请勿使用“激活”或“选择”移动ActiveCell,只需直接访问该范围。
  4. 循环时,构造循环必须尽可能少地执行测试。如果循环必须始终执行一次,那么您需要一个测试后循环。
  5. 确保您锁定了Excel界面,因为正在运行的事件和屏幕更新等会降低您的代码速度。 (特别是活动。)
  6. 最后,我注意到您正在对“站点ID”的情况做出假设,除非没有其他可能的方法,否则最好进行不区分大小写的比较。如果您知道它会以某种方式进行Cased,您当然可以删除我添加的对LCase $的调用。
  7. 我重构了原始代码,为您提供了一些这些想法的示例。在不知道您的数据布局的情况下,很难确定此代码是否100%有效,因此我会仔细检查它是否存在逻辑错误。但它应该让你开始。

    Public Sub PageBreak(ByRef wstWorksheet As Excel.Worksheet, ByVal pctProgress As ProgressCtl.ProgressContro)
            Const lngColSiteID_c As Long = 2&
            Const lngColSiteIDSecondary_c As Long = 1&
            Const lngOffset_c As Long = 1&
            Dim breaksMoved As Boolean
            Dim lngRowBtm As Long
            Dim lngRow As Long
            Dim p As Excel.HPageBreak
            Dim i As Integer
            Dim passes As Long
            Dim lngHBrksUprBnd As Long
            LockInterface True
            ' Marks that no rows/columns are to be repeated on each page
            wstWorksheet.Activate
            wstWorksheet.PageSetup.PrintTitleRows = vbNullString
            wstWorksheet.PageSetup.PrintTitleColumns = vbNullString
    
    
            'If this isn't performed beforehand, then the HPageBreaks object isn't available
            '***Not true:)***
    
            'ActiveWindow.View = xlPageBreakPreview
    
            'Defaults the print area to be the entire sheet
            wstWorksheet.DisplayPageBreaks = False
            wstWorksheet.PageSetup.PrintArea = vbNullString
    
            ' add breaks after each site
            lngRowBtm = wstWorksheet.UsedRange.Rows.Count
            For lngRow = 4& To lngRowBtm
                'LCase is to make comparison case insensitive.
                If LCase$(wstWorksheet.Cells(lngRow, lngColSiteID_c).value) = "site id" Then
                    wstWorksheet.Cells(lngRow, lngColSiteID_c).PageBreak = xlPageBreakManual
                End If
                pctProgress.ProgressText = ("Row " & CStr(lngRow)) & (" of " & CStr(lngRowBtm))
            Next
    
            lngHBrksUprBnd = wstWorksheet.HPageBreaks.Count - lngOffset_c
            Do  'Using post test.
                passes = passes + lngOffset_c
                breaksMoved = False
                For i = 1 To lngHBrksUprBnd
                    Set p = wstWorksheet.HPageBreaks.Item(i)
                    'Move the intended break point up to the first blank section
                    lngRow = p.Location.Row - lngOffset_c
                    For lngRow = p.Location.Row - lngOffset_c To 1& Step -1&
                        'Checking the LenB is faster than a string check.
                        If LenB(wstWorksheet.Cells(lngRow, lngColSiteIDSecondary_c).Formula) = 0& Then
                            lngRow = lngRow - lngOffset_c
                            If LCase$(wstWorksheet.Cells(lngRow, lngColSiteIDSecondary_c).value) = "site id" Then
                                breaksMoved = True
                                wstWorksheet.HPageBreaks.Add wstWorksheet.Cells(lngRow, lngColSiteIDSecondary_c)
                            End If
                            Exit For
                        End If
                    Next
                    pctProgress.ProgressText = "Set break point " & (CStr(passes) & "." & CStr(i))
                Next
            Loop While breaksMoved
            LockInterface False
        End Sub
    
        Private Sub LockInterface(ByVal interfaceOff As Boolean)
            With Excel.Application
                If interfaceOff Then
                    .ScreenUpdating = False
                    .EnableEvents = False
                    .Cursor = xlWait
                    .StatusBar = "Working..."
                Else
                    .ScreenUpdating = True
                    .EnableEvents = True
                    .Cursor = xlDefault
                    .StatusBar = False
                End If
            End With
        End Sub
    

答案 1 :(得分:2)

简单的答案是您使用ActiveCellSelect以及Activate。 Excel实际上在代码运行时选择单元格,使代码运行得更慢(正如您所注意到的)。

我建议使用Range作为参考,并在“内存中”执行所有测试。

调整跟踪范围(dim rngCurrentCell as range)并使用该范围代替选择单元格。

因此,对于代码Select中首次出现Range("A3").Select,您可以将其设置为Set rngCurrentCell = Range("A3")。对于Next B4系列也一样。

然后:

' add breaks after each site
Do While ActiveCell.Row <= wstWorksheet.UsedRange.Rows.Count 

If ActiveCell.FormulaR1C1 = "Site ID" Then
ActiveCell.PageBreak = xlPageBreakManual    
End If    
' Offset the row by one and set our new range
set rngCurrentCell = rngCurrentCell.Offset(1, 0)

pctProgress.ProgressText = "Row " & CStr(ActiveCell.Row) & " of " & CStr(wstWorksheet.UsedRange.Rows.Count)

Loop

等等。

现在测试值使用与ActiveCell相同的语法。

如果您有任何疑问,请与我们联系。

答案 2 :(得分:1)

我快速查看了您的代码,我的第一个想法是这一行:

pctProgress.ProgressText =“设置分页符”&amp; CStr(i)&amp; “of”&amp; CStr的(shtDeliveryVariance.HPageBreaks.Count)

可能是导致某些延迟的原因。此代码的位置意味着系统必须重新计算.Count值,因为它在代码中的循环开始时出现,但这种重新计算不会发生在原始代码中。

其他想法:

根据电子表格的大小,外出并重新测量此值可能会减慢速度。为什么不在实际执行添加新的中断而不是让系统进行计数时手动递增中断计数跟踪变量,或者在循环中除去计数(因为在此期间您不会更新显示这个过程)并将分页计数放入其自己的代码段中,该代码段在整个格式化过程结束时通过内容运行,此时可以通过一次调用轻松确定最终的分页数?