【ExcelVBA】狭いフィルターを解決する

Japanese

はじめに

Excelのフィルターを使用することはお仕事でよくある。必須ともいえる機能ですが、使用していてフィルターのウィンドウがとても小さいなと感じる場面がありませんか?

かく言う私は、セルの値が長くなれば長くなるほど毎回のようにイライラしながら横にスライドしてフィルターをかけます。

このように長い値になればなるだけ見にくいじゃないですか。

なので今回は既存のフィルターを使用しない方法でフィルターをする方法を紹介します。

フィルターウィンドウ

絞り込み

作成したものについて紹介します。
わかりやすく簡潔な表を作成し、フィルターボタンを押しました。

すると、フィルターウィンドウが表示され入力項目が出現します。
操作方法は、、、

1.見出し範囲の設定
2.フィルターしたい列を指定
3.フィルター内にある値を指定
4.適用

以上が基本操作になります。
解除したい場合は「解除」ボタンを押せば解除されます。

連続でフィルターを適用することも可能です。
一度フィルターしたあと、再度選択し適用することでフィルターできます。

部分検索

チェックボックスにチェックを入れ、検索したいフィルター列を選択。
「開」と入力して適用を押すと画像のように検索することもできます。

使用上の注意

通常のフィルターとこのフィルターウィンドウは別々の動作をします。
なのでフィルターを適用しても見出し範囲は適用されません。

見出し範囲は自身で入力して更新ボタンを押してください。

一度、更新ボタンを押すとExcel内部で値を保管してくれるので、一度ファイルを閉じても問題なく再度開いたときに同じ値がセットされます。

配布について

作成したものを以下のリンクからダウンロードすることができます。
皆さんのお仕事に役に立てたらなと思います。

特に制限はありませんが、良識の範囲でご使用頂けたらと思います。

コード

とはいえ、そんなものをダウンロードしたくない!
という人向けにコードを記載しておきます。

よくわからないという人は以下の記事から読んでみるのもよいかもしれません。

フォーム

Option Explicit

Private mLoading As Boolean
Private Const MAX_CBO_ITEMS As Long = 300

Private Sub txtKeyword_Change()

End Sub

Private Sub UserForm_Initialize()
    Me.StartUpPosition = 1

    cboColumn.Clear
    cboValue.Clear
    txtKeyword.Value = ""
    chkPartial.Value = False

    gFilterRange = LoadSavedFilterRange()
    txtFilterRange.Value = gFilterRange

    RefreshFilterItems
End Sub

Private Sub txtFilterRange_AfterUpdate()
    ' 更新ボタンで反映するため、ここでは何もしない
End Sub

Private Sub cboColumn_Change()
    If mLoading Then Exit Sub
    LoadUniqueValues
End Sub

Private Sub cmdApply_Click()
    Dim ws As Worksheet
    Dim headerRange As Range
    Dim dataRange As Range
    Dim fieldIndex As Long
    Dim criteria As String
    
    On Error GoTo EH
    
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
    
    Set ws = ActiveSheet
    Set headerRange = GetHeaderRange(True)
    
    gFilterRange = Trim(txtFilterRange.Value)
    SaveFilterRange gFilterRange
    
    If headerRange Is Nothing Then GoTo ExitPoint
    
    If cboColumn.ListIndex = -1 Then
        MsgBox "フィルターする列を選択してください。", vbExclamation
        GoTo ExitPoint
    End If
    
    fieldIndex = cboColumn.ListIndex + 1
    Set dataRange = GetFilterRangeFromHeader(headerRange)
    
    If Trim(txtKeyword.Value) <> "" Then
        criteria = Trim(txtKeyword.Value)
    ElseIf Trim(cboValue.Value) <> "" Then
        criteria = Trim(cboValue.Value)
    Else
        MsgBox "検索文字を入力するか、候補を選択してください。", vbExclamation
        GoTo ExitPoint
    End If
    
    If chkPartial.Value Then
        dataRange.AutoFilter Field:=fieldIndex, Criteria1:="=*" & EscapeFilterText(criteria) & "*"
    Else
        dataRange.AutoFilter Field:=fieldIndex, Criteria1:="=" & EscapeFilterText(criteria)
    End If
    
    LoadUniqueValues
    
ExitPoint:
    Application.Calculation = xlCalculationAutomatic
    Application.EnableEvents = True
    Application.ScreenUpdating = True
    Exit Sub

EH:
    MsgBox Err.Description, vbExclamation
    Resume ExitPoint
End Sub

Private Sub cmdClearCurrent_Click()
    Dim headerRange As Range
    Dim dataRange As Range
    Dim fieldIndex As Long
    
    Set headerRange = GetHeaderRange(True)
    If headerRange Is Nothing Then Exit Sub
    
    If cboColumn.ListIndex = -1 Then
        MsgBox "解除する列を選択してください。", vbExclamation
        Exit Sub
    End If
    
    fieldIndex = cboColumn.ListIndex + 1
    Set dataRange = GetFilterRangeFromHeader(headerRange)
    
    dataRange.AutoFilter Field:=fieldIndex
    
    txtKeyword.Value = ""
    cboValue.ListIndex = -1
    
    LoadUniqueValues
    
'    MsgBox "選択列のフィルターを解除しました。", vbInformation
End Sub

' 更新
Private Sub cmdRefresh_Click()
    gFilterRange = Trim(txtFilterRange.Value)
    SaveFilterRange gFilterRange
    RefreshFilterItems
End Sub

' 解除
Private Sub cmdClear_Click()
    Dim ws As Worksheet
    
    Set ws = ActiveSheet
    
    On Error Resume Next
    If ws.FilterMode Then
        ws.ShowAllData
    End If
    On Error GoTo 0
    
    txtKeyword.Value = ""
    cboValue.ListIndex = -1
End Sub

' 閉じる
Private Sub cmdClose_Click()
    Me.Hide
End Sub

Private Sub LoadHeadersFromRange()
    Dim headerRange As Range
    Dim c As Range
    
    cboColumn.Clear
    cboValue.Clear
    
    Set headerRange = GetHeaderRange(False)
    If headerRange Is Nothing Then Exit Sub
    
    For Each c In headerRange.Cells
        cboColumn.AddItem CStr(c.Value)
    Next c
    
    If cboColumn.ListCount > 0 Then
        cboColumn.ListIndex = 0
    End If
End Sub

Private Sub LoadUniqueValues()
    Static isRunning As Boolean
    
    Dim ws As Worksheet
    Dim headerRange As Range
    Dim dataRange As Range
    Dim targetRange As Range
    Dim visibleCells As Range
    Dim area As Range
    Dim cell As Range
    Dim fieldIndex As Long
    Dim v As String
    Dim key As Variant
    Dim dict As Object
    Dim scanCount As Long
    
    Const MAX_SCAN_CELLS As Long = 5000
    
    If isRunning Then Exit Sub
    If mLoading Then Exit Sub
    
    isRunning = True
    On Error GoTo ExitHandler
    
    cboValue.Clear
    
    Set headerRange = GetHeaderRange(False)
    If headerRange Is Nothing Then GoTo ExitHandler
    If cboColumn.ListIndex = -1 Then GoTo ExitHandler
    
    Set ws = ActiveSheet
    Set dataRange = GetFilterRangeFromHeader(headerRange)
    
    fieldIndex = cboColumn.ListIndex + 1
    
    If dataRange.Rows.Count <= 1 Then GoTo ExitHandler
    Set targetRange = dataRange.Columns(fieldIndex).Offset(1, 0).Resize(dataRange.Rows.Count - 1, 1)
    
    Set dict = CreateObject("Scripting.Dictionary")
    
    On Error Resume Next
    Set visibleCells = targetRange.SpecialCells(xlCellTypeVisible)
    On Error GoTo ExitHandler
    
    If visibleCells Is Nothing Then GoTo ExitHandler
    
    For Each area In visibleCells.Areas
        For Each cell In area.Cells
            scanCount = scanCount + 1
            If scanCount > MAX_SCAN_CELLS Then GoTo FillCombo
            
            v = Trim$(CStr(cell.Value))
            If v <> "" Then
                If Not dict.Exists(v) Then
                    dict.Add v, v
                    If dict.Count >= MAX_CBO_ITEMS Then GoTo FillCombo
                End If
            End If
        Next cell
    Next area
    
FillCombo:
    For Each key In dict.Keys
        cboValue.AddItem CStr(key)
    Next key

ExitHandler:
    isRunning = False
End Sub

Private Function GetHeaderRange(ByVal showMessage As Boolean) As Range
    Dim ws As Worksheet
    Dim s As String
    Dim rg As Range
    
    Set ws = ActiveSheet
    s = Trim(txtFilterRange.Value)
    
    If s = "" Then
        If showMessage Then
            MsgBox "見出し範囲を入力してください。例: A1:C1", vbExclamation
        End If
        Exit Function
    End If
    
    On Error Resume Next
    Set rg = ws.Range(s)
    On Error GoTo 0
    
    If rg Is Nothing Then
        If showMessage Then
            MsgBox "見出し範囲が不正です。例: A1:C1", vbExclamation
        End If
        Exit Function
    End If
    
    If rg.Rows.Count <> 1 Then
        If showMessage Then
            MsgBox "見出し範囲は1行で指定してください。例: A1:C1", vbExclamation
        End If
        Exit Function
    End If
    
    Set GetHeaderRange = rg
End Function

Private Function GetFilterRangeFromHeader(ByVal headerRange As Range) As Range
    Dim ws As Worksheet
    Dim firstCol As Long
    Dim lastCol As Long
    Dim topRow As Long
    Dim lastDataRow As Long
    Dim c As Long
    Dim r As Long
    
    Set ws = headerRange.Worksheet
    
    firstCol = headerRange.Column
    lastCol = headerRange.Column + headerRange.Columns.Count - 1
    topRow = headerRange.Row
    lastDataRow = topRow
    
    For c = firstCol To lastCol
        r = ws.Cells(ws.Rows.Count, c).End(xlUp).Row
        If r > lastDataRow Then
            lastDataRow = r
        End If
    Next c
    
    Set GetFilterRangeFromHeader = ws.Range(ws.Cells(topRow, firstCol), ws.Cells(lastDataRow, lastCol))
End Function

Private Function GetDefaultHeaderRangeAddress() As String
    Dim ws As Worksheet
    Dim rg As Range
    
    Set ws = ActiveSheet
    
    On Error Resume Next
    If TypeName(Selection) = "Range" Then
        If Selection.Rows.Count = 1 Then
            Set rg = Selection
        End If
    End If
    On Error GoTo 0
    
    If rg Is Nothing Then
        Set rg = ws.Range("A1").CurrentRegion.Rows(1)
    End If
    
    GetDefaultHeaderRangeAddress = rg.Address(False, False)
End Function

Private Function ColumnLetter(ByVal colNum As Long) As String
    ColumnLetter = Replace(Cells(1, colNum).Address(False, False), "1", "")
End Function

Private Function EscapeFilterText(ByVal s As String) As String
    s = Replace(s, "~", "~~")
    s = Replace(s, "*", "~*")
    s = Replace(s, "?", "~?")
    EscapeFilterText = s
End Function

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = vbFormControlMenu Then
        Cancel = True
        Me.Hide
    End If
End Sub

Private Sub RefreshFilterItems()
    Dim headerRange As Range
    
    Set headerRange = GetHeaderRange(True)
    If headerRange Is Nothing Then Exit Sub
    
    mLoading = True
    
    cboColumn.Clear
    cboValue.Clear
    txtKeyword.Value = ""
    
    LoadHeadersFromRange
    
    mLoading = False
    
    If cboColumn.ListCount > 0 Then
        LoadUniqueValues
    End If
End Sub

標準モジュール

Option Explicit

Public gFilterRange As String
Private Const FILTER_RANGE_NAME As String = "_frmFilterRange"

' フィルターウィンドウ表示
Public Sub ShowFilterForm()
    With frmFilter
        .StartUpPosition = 0
        .Left = Application.Left + (Application.Width - .Width) / 2
        .Top = Application.Top + (Application.Height - .Height) / 2
        .Show
    End With
End Sub

Public Function LoadSavedFilterRange() As String
    Dim s As String
    
    On Error Resume Next
    s = ThisWorkbook.Names(FILTER_RANGE_NAME).RefersTo
    On Error GoTo 0
    
    If s <> "" Then
        If Left$(s, 1) = "=" Then s = Mid$(s, 2)
        If Left$(s, 1) = """" And Right$(s, 1) = """" Then
            s = Mid$(s, 2, Len(s) - 2)
        End If
        s = Replace(s, """""", """")
    End If
    
    If Trim$(s) = "" Then s = "A1:C1"
    
    LoadSavedFilterRange = s
End Function

Public Sub SaveFilterRange(ByVal rangeText As String)
    Dim refersToText As String
    
    rangeText = Trim$(rangeText)
    If rangeText = "" Then Exit Sub
    
    refersToText = "=""" & Replace(rangeText, """", """""") & """"
    
    On Error Resume Next
    ThisWorkbook.Names(FILTER_RANGE_NAME).Delete
    On Error GoTo 0
    
    ThisWorkbook.Names.Add Name:=FILTER_RANGE_NAME, RefersTo:=refersToText
    ThisWorkbook.Names(FILTER_RANGE_NAME).Visible = False
End Sub
タイトルとURLをコピーしました