Разрывы страниц Excel через VBA - PullRequest
3 голосов
/ 12 июня 2009

В рамках капитального ремонта генератора отчетов я увидел то, что я считал неэффективным кодом. Эта часть кода выполняется после генерации основного отчета, чтобы установить разрывы страниц в логических позициях. Критерии таковы:

  • Каждый сайт начинается на новой странице.
  • Группам запрещается разбивать страницы.

Код соответствует указанному выше формату: 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 () с приблизительными сроками это будет на 1 секунду медленнее в моем коротком отчете за 4 сайта и более сложном отчете за 15 сайтов. Они имеют 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

Один проход и более элегантный тоже. Но насколько это быстрее? В маленьком тесте это занимает 54 секунды по сравнению с первоначальными 45 секундами, а в более крупном тесте мой код снова работает медленнее - от 153 до 130 секунд. И это в среднем за 3 прогона тоже.

Итак, мои вопросы: Почему мой новый код намного медленнее оригинального, несмотря на то, что мой выглядит быстрее и , что я могу сделать, чтобы ускорить медлительность кода ?

Примечание : Обновление экрана и т. Д. Уже отключено, как и расчет и т. Д.

Ответы [ 3 ]

13 голосов
/ 13 июня 2009

Я вижу возможности для улучшения в нескольких местах в вашем коде:

  1. Не обращайтесь к свойствам, которые реализуются медленно, например, usedrange.rows.count более одного раза (особенно внутри цикла), если только вы не думаете, что они могут иметь изменения. Вместо этого сохраните их в переменной.
  2. Не делайте текстовые сравнения, если вы можете избежать этого (например: .Value = ""), вместо этого используйте функцию LenB для проверки на пустоту, она будет выполняться быстрее, так как она просто читает длину заголовка строки вместо запуск в байтовое сравнение строк. (Вам может понравиться это для чтения.)
  3. Не используйте «Активировать» или «Выбрать» для перемещения по ActiveCell, просто получите прямой доступ к диапазону.
  4. При зацикливании структурируйте ваш цикл так, чтобы он выполнял как можно меньше тестов. Если цикл должен всегда выполняться один раз, то вам нужен цикл после тестирования.
  5. Убедитесь, что у вас заблокирован интерфейс Excel, так как выполнение событий, обновление экрана и т. Д. Могут значительно замедлить ваш код. (Особенно события.)
  6. Наконец, я заметил, что вы делаете предположения о случае «идентификатора сайта», если нет другого способа, которым это могло бы быть рассмотрено иначе, лучше проводить сравнение без учета регистра. Если вы точно знаете, что это произойдет именно так, вы, конечно, можете удалить вызовы в LCase $, которые я добавил.

Я произвел рефакторинг оригинального кода, чтобы дать вам пример некоторых из этих идей. Не зная вашего макета данных, трудно быть уверенным, что этот код верен на 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
2 голосов
/ 13 июня 2009

Простой ответ: вы используете ActiveCell и Select и Activate. Excel фактически выбирает ячейки во время выполнения кода, что замедляет выполнение кода (как вы заметили).

Я бы рекомендовал использовать Range в качестве эталона и выполнять все тесты "в памяти".

Уменьшите диапазон для отслеживания (dim rngCurrentCell as range) и используйте его вместо выделения ячеек.

Таким образом, для первого появления Select в вашем коде Range("A3").Select вы должны установить его как Set rngCurrentCell = Range("A3"). То же самое для следующей строки 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.

Если у вас есть какие-либо вопросы, дайте мне знать.

1 голос
/ 12 июня 2009

Я быстро просмотрел ваш код, и я впервые подумал, что эта строка:

pctProgress.ProgressText = "Установка разрыва страницы" и CStr (i) & "of" & CStr (shtDeliveryVariance.HPageBreaks.Count)

может быть причиной некоторой задержки. Расположение этого кода означает, что система должна пойти и пересчитать значение .Count, так как оно происходит в начале цикла в вашем коде, но этот пересчет не происходит в оригинале.

Другие мысли:

В зависимости от размера электронной таблицы выход и повторное измерение этого значения могут замедлять процесс. Почему бы просто не увеличить вручную переменную отслеживания количества разрывов, когда вы фактически добавляете новый разрыв вместо того, чтобы система пошла и посчитала его, или избавилась от счета в цикле (так как вы не обновляете отображение во время этот процесс) и поместить подсчет разрывов страниц в отдельный сегмент кода, который проходит через контент в конце всего процесса форматирования, когда конечное число разрывов страниц можно легко определить с помощью одного вызова?

...