3

レポートジェネレータのオーバーホールの一環として、私は非効率的なコードであると信じているものを見ました。コードのこの部分は、メインレポートが生成された後に実行され、ページ分割を論理的な位置に設定します。基準は次のとおりです。

  • 各サイトは新しいページから始まります。
  • グループをページ間で分割することは許可されていません。

コードは上記の形式に従います。これらのジョブを実行する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

改善の余地があるので、これを修正することにしました。新しい要件の1つとして、レポートを必要とする人々は、印刷する前に手動でページを削除していました。そこで、別のページにチェックボックスを追加し、チェックした項目をコピーしました。それを簡単にするために、名前付き範囲を使用しました。最初の要件を満たすために、これらの名前付き範囲を使用しました。

' 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などは、Calculationなどと同様にすでにオフになっています。

4

3 に答える 3

13

コードのいくつかの箇所に改善の余地があります。

  1. 変更があると思われる場合を除き、usedrange.rows.count など、実装が遅いプロパティには (特にループ内で) 2 回以上アクセスしないでください。代わりに、それらを変数に格納します。
  2. 回避できる場合はテキスト比較を行わないでください (例: .Value = "")。代わりに、LenB 関数を使用して空かどうかを確認してください。バイト単位の文字列比較。(これを読んで楽しむかもしれません。)
  3. 「Activate」または「Select」を使用して ActiveCell 内を移動しないでください。範囲に直接アクセスしてください。
  4. ループするときは、実行するテストができるだけ少なくなるようにループを構成します。ループを常に 1 回実行する必要がある場合は、テスト後のループが必要です。
  5. イベントの実行や画面の更新などにより、コードが大幅に遅くなる可能性があるため、Excel インターフェイスがロックされていることを確認してください。(特にイベント。)
  6. 最後に、「Site ID」の大文字と小文字について推測していることに気付きました。他の方法で区別できない場合を除き、大文字と小文字を区別しない比較を行うことをお勧めします。そのようにケース化されるという事実がわかっている場合は、もちろん、追加した 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
于 2009-06-12T21:00:36.007 に答える
2

簡単な答えは、ActiveCellandSelectとを使用することです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

ご不明な点がございましたら、お知らせください。

于 2009-06-12T20:23:02.697 に答える
1

私はあなたのコードをざっと見ましたが、最初に考えたのは次の行です。

pctProgress.ProgressText = "改ページの設定 " & CStr(i) & " of " & CStr(shtDeliveryVariance.HPageBreaks.Count)

一部遅延の原因となる場合があります。このコードの場所は、システムが .Count 値を再計算する必要があることを意味します。これは、コードのループの先頭にあるためです。ただし、この再計算は元のコードでは発生しません。

他の考え:

スプレッドシートのサイズによっては、外に出てこの値を再測定すると、速度が低下する場合があります。システムに行ってカウントさせるのではなく、新しいブレークの追加を実際に実行するときに、ブレークカウント追跡変数を手動でインクリメントしないのはなぜですか。このプロセス) 改ページのカウントを独自のコード セグメントに入れます。このコード セグメントは、1 回の呼び出しで改ページの最終的な数を簡単に決定できるときに、書式設定プロセス全体の最後にコンテンツを実行しますか?

于 2009-06-12T17:03:14.783 に答える