Excel VBAでSelection(選択範囲)を列単位、行単位で処理する

ExcelのVBAで選択されているセル範囲を列単位、行単位で処理する方法について書いてみます。


選択範囲を処理する方法

Excelで選択された範囲をVBAではApplication.SelectionでRangeオブジェクトとして取得できます。Selectionの処理方法について検索すると次のようなページがヒットします。

選択範囲の操作

しかし、この方法では、一つの行に対して列数分だけ繰り返し同じ処理が行われてしまいます。また、ユーザーがCtrlキーを押しながら複数の範囲(Area)を指定した場合には破綻します。

ちなみに、Selection.Rows.Countは最初のエリア(Area)しかCountの対象としません。残りのエリアは対象外です。

Range.Rows プロパティ (Excel)


単純な行番号比較はだめ

Selection のアイテム番号は左上から右方向に振られているように見えたので次のプロシージャを実行してみましたがダメでした。

Sub uWrong()
    Dim uCell As Range
    Dim uMax As Long
    
    For Each uCell In Selection
        If uCell.Row > uMax Then
            uMax = uCell.Row
            uCell = 1
        End If
    Next
End Sub

未処理の行があります。

次のプロシージャーで、selection で取得した順番を表示してみました。

Sub uFillAll()
    Dim uCell As Range
    Dim i As Long
    
    For Each uCell In Selection
        i = i + 1
        uCell = i
    Next
End Sub

結果は次の通り。

これは次の順番でCtrlキーを押しながら範囲を選択したためです。

このように、Selectionは指定したエリア順に処理されるようで、上から順番に処理されるわけではないようです。

SelectionはRangeのコレクションを順番に返しているだけなのでしょう。


UNIQUEとSORTを使う

2022/12/10 追記

このページがそれなりにヒットしているので見直してみたところ、もっと簡単な方法があることに気が付きました。今どきの Excel の場合、 UNIQUE と SORT が使えます。それを使って、作り直してみました。

コード:

Public Sub uGetSelectedRows()
    Dim uSelected As Range
    Dim uRowNumbers() As Long
    Dim i As Long
    Dim uUnique() As Variant
    Dim uSorted() As Variant
    
    Const uColumn As Boolean = True
    Const uAscend As Integer = 1
    
    For Each uSelected In Selection
        ReDim Preserve uRowNumbers(i)
        uRowNumbers(i) = uSelected.Row
        i = i + 1
    Next
    
    uUnique = WorksheetFunction.Unique(uRowNumbers, uColumn)
    uSorted = WorksheetFunction.Sort(uUnique, 1, uAscend, uColumn)
End Sub

実行結果:


サクッとできました。

選択されたセルの行番号をuRowNumbersに保存し、それをUniqueで重複排除して、Sortで並べ替えています。

uSortedを使えば、選択されたセルの行を上から順番に重複せずに処理していくことができます。列順に処理したいならば、uSelected.RowをuSelected.Columnに変更します。


1次元配列は、列方向とみなされるようなので、基準に列指定(uColumn=True)を行っています。最初この指定をしなかったら、ユニークにならず、?でした。

参考にしたのは次のページです。

VBAでシート関数使用時の配列要素数制限 Excelの神髄

SORT関数でデータを並べて取り出す できるネット


以降は前回作成した内容です。ご参考までに。

処理した行番号を保存して処理済みかチェックする

やむを得ないので、処理した行を記録して、処理済みかどうかチェックしながら処理する事にしました。

記録にはCollectionを使用しています。

Sub uFillRowOnce()
    Dim uCell As Range
    Dim uColl As Collection
    Dim i As Long
    
    Set uColl = New Collection
    
    For Each uCell In Selection
        If Not uContains(uCell.Row, uColl) Then
            i = i + 1
            uCell = i
            uColl.Add uCell.Row
        End If
    Next
End Sub

こちらは値がコレクションに含まれているかどうかのチェックを行う関数。

Function uContains(ByVal uValue As Variant, ByRef uColl As Collection) As Boolean
    Dim uItem As Variant
    
    For Each uItem In uColl
        If uItem = uValue Then
            uContains = True
            Exit Function
        End If
    Next
End Function

うまく選択されました。

こちらは列単位で処理するバージョン

Sub uFillColumnOnce()
    Dim uCell As Range
    Dim uColl As Collection
    Dim i As Long
    
    Set uColl = New Collection
    
    For Each uCell In Selection
        If Not uContains(uCell.Column, uColl) Then
            i = i + 1
            uCell = i
            uColl.Add uCell.Column
        End If
    Next
End Sub

うまく処理できています。

ただし、この方法だと行が処理される順番はエリアの指定順次第になります。Collectionはソートには不向きだと思うので、配列のバージョンも考えてみました。

Sub uFillRowOnceArrayVer()
    Dim uArea As Range
    Dim uRows As Long
    Dim uRow As Range
    Dim uCell As Range
    Dim uRowArray() As Long
    Dim i As Long
    Dim ii As Long
    
    For Each uArea In Selection.Areas
        uRows = uRows + uArea.Rows.Count
    Next

    ReDim uRowArray(uRows)
    
    For Each uCell In Selection
        If Not uContainsArray(uCell.Row, uRowArray) Then
            uRowArray(i) = uCell.Row
            i = i + 1
            uCell = i
        End If
    Next
    
    For ii = 0 To i - 1
        Debug.Print uRowArray(ii)
    Next
End Sub

配列に含まれているかどうかをチェックする関数。

Function uContainsArray(ByVal uValue As Variant, ByRef uArray As Variant) As Boolean
    Dim i As Long
    For i = LBound(uArray) To UBound(uArray)
        If uArray(i) = uValue Then
            uContainsArray = True
            Exit Function
        End If
    Next
End Function

Debug.Printの結果は次の通りで、期待通りです。

 1  2  3  4  6  7  8  9  5 

これをソートすれば、順番に並んだ処理すべき行番号の出来上がりです。ソート方法は次のサイトなどを参考にするとよいでしょう。

VBA 配列の並び替え

なお、最初のFor文では、Areaの行数を合計しています。本当はSelection.Rows.CountでSelectionの真の(実際に使用している)行数が取れると良いのですが、上にかいたようにSelection.Rows.Countが返すのは最初のエリアの行数のみです。

とりあえずオーバーフローさえしなければよいかと上のようなコードを書いてみました。


というわけで、Selectionで指定された範囲を重複なく行単位、列単位で処理する方法でした。


コメント

アクセス数の多い投稿

セキュリティ対策ソフトのノートンが詐欺ソフトまがいになってしまってショック

ZIPファイルを開こうとすると、展開を完了できません、と言われる

Excel 2019 クエリが原因で日本語入力の一文字目が勝手に確定する

Excelのテーブルに行や列を挿入する際のエラー

オカムラ家具のOAチェアー、コンテッサを分解清掃

突然滅茶苦茶遅くなったPCがWindows Updateのキャッシュクリアで復活

ChatGPTが日本語からVBAのコードを生成できてたまげる

Windows セキュリティーのビックリマークが消えない

Power Automate Desktopでブラウザでダウンロードしたファイルを処理する

Excel VBAからODBCを使ってデータを簡単に取得する