タブ式テキストエディタ(11) 検索・置換システムをつくる

 検索にはいろいろな方法があるのですが、ここでは既存のメソッド利用した単純な検索方法のみを使用することにします。なので、長大な文書(特に置換)だとパフォーマンスが落ちてちょっときついかもしれません。
 ダイアログボックスの各コントロールは以下のように配置しました。今回はデザイナを使っています。

FindDialog クラス

 ダイアログ起動時には、初期化ファイルから、位置"LoadPoint"と検索条件"Machcase"の値を取り出して、前回終了時の状態を復元することにします。また、検索置換ダイアログはひとつしか起動できないよう、フラグ"Exists"を使用して初回起動時に立てるようにします。
 検索語句と置換語句は、起動中は値を保持することにします。前回検索(置換)した語句がリスト化されて表示されます。ここで使用している"StringCollection"は、以前つくったオリジナルのクラスです。

Public Class FindDialog
    Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "
        ... (略) ...
#End Region

    ' 変数
    Private m_tabs As TabControl        ' 対象のタブコントロール
    Private m_edit As ExpandedTextControl    ' 対象のテキストボックス
    Private m_findwords As StringCollection    ' 検索語句の履歴
    Private m_replacewords As StringCollection  ' 置換語句の履歴
    Private Shared m_exists As Boolean = False  ' 起動済みフラグ
    Private Shared m_point As Point        ' 起動時の位置
    Private Shared m_matchcase As Boolean    ' 大文字小文字を区別するか

    ' プロパティ : Exists(ダイアログが起動済みかどうか)
    Public Shared Property Exists() As Boolean
        Get
            Return m_exists
        End Get
        Set(ByVal Value As Boolean)
            m_exists = Value
        End Set
    End Property

    ' プロパティ : LoadPoint(ダイアログ読み込み時の位置)
    Public Shared Property LoadPoint() As Point
        Get
            Return m_point
        End Get
        Set(ByVal Value As Point)
            m_point = Value
        End Set
    End Property

    ' プロパティ : MatchCase(大文字と小文字を区別するか)
    Public Shared Property MatchCase() As Boolean
        Get
            Return m_matchcase
        End Get
        Set(ByVal Value As Boolean)
            m_matchcase = Value
        End Set
    End Property

    ' New コンストラクタ
    ' ... 引数 owner : 親フォーム
    ' ... 引数 loadpoint : 読み込み時の位置
    ' ... 引数 matchcase : 大文字と小文字を区別するかどうか
    Public Sub New(ByRef owner As Form, _
            ByRef loadpoint As Point, _
            ByVal matchcase As Boolean)
        MyBase.New()
        InitializeComponent()
        Me.Owner = owner
        m_point = loadpoint
        m_matchcase = matchcase
        m_exists = True
    End Sub

    ' Load : ロード
    Private Sub FindDialog_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
            Handles MyBase.Load
        Me.Location = LoadPoint
        chkMatchcase.Checked = m_matchcase
        UpdateButtonState()
    End Sub

    ' Closing : アンロード
    Private Sub FindDialog_Closing(ByVal sender As Object, _
        ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
        m_point = Me.Location
        m_matchcase = chkMatchcase.Checked
        m_exists = False
        m_edit.Focus()
    End Sub

   ' UpdateButtonState : ボタンの状態の更新
    Private Sub UpdateButtonState()
        If cbFind.Text = "" Then
            bnFind.Enabled = False
            bnReplace.Enabled = False
            bnReplaceAll.Enabled = False
        Else
            bnFind.Enabled = True
            bnReplace.Enabled = Not cbReplace.Text.Equals("")
            bnReplaceAll.Enabled = Not cbReplace.Text.Equals("")
        End If
    End Sub

    ' 検索語コンボボックス : テキスト変更時
    Private Sub cbFind_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
            Handles cbFind.TextChanged
        UpdateButtonState()
    End Sub

    ' 置換語コンボボックス : テキスト変更時
    Private Sub cbReplace_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) 
            Handles cbReplace.TextChanged
        UpdateButtonState()
    End Sub
End Class 

 "UodateButtonState"は、検索語句や置換語句の入力状態に応じてボタンの使用可・不可を切り替えるための関数です。

検索(置換)語句のリストの保守

 検索・置換語句のリストを保守するためのコードです。一度使用した語句はそれぞれのコレクションクラスへ格納し、コンボボックスのリストへ反映させています。

' UpdateWordsList : 検索(置換)語句リストの更新
' ... 引数 cb : 対象のコンボボックス(検索コンボボックスか置換コンボボックスか)
' ... 引数 words : 対象のリスト(検索語句リストか置換語句リストか)
Private Sub UpdateWordsList(ByRef cb As ComboBox, ByRef words As StringCollection)
    Dim strWord As String = cb.Text    ' 追加する語句
    Dim nIndex As Integer    ' インデックス
    Const nMax As Integer = 10    ' 最大項目数
    If strWord = "" Then Exit Sub

    ' リストの項目数によって処理を分岐
    If words.Count > 0 Then
        ' 追加する語句が既出の場合は削除
        nIndex = words.IndexOf(strWord)
         If nIndex >= 0 Then
            cb.Items.RemoveAt(nIndex)
            words.RemoveAt(nIndex)
        End If
        ' 語句をリストの先頭に挿入
        cb.Items.Insert(0, strWord)
        words.Insert(strWord, 0)
    Else
        ' 語句を追加
        cb.Items.Add(strWord)
        words.Insert(strWord)
    End If

    ' 最大項目数を越えたら古いものを削除
    If words.Count > nMax Then
        cb.Items.RemoveAt(nMax)
        words.RemoveAt(nMax)
    End If
    cb.SelectedIndex = 0
End Sub 

 ところで、検索・置換語句はダイアログ側で保持するのではなくメインフォーム側で保持するようにします。ダイアログ側で保持すると、ダイアログが閉じた時に当然データも破棄されてしまいます。ダイアログ側はメインフォーム側のデータを参照するだけです。こうするとアプリケーション起動中はリストの値を保持できるようになります。

メインフォーム側の変数の宣言
    Private m_findwords As StringCollection        ' 検索語句集
    Private m_replacewords As StringCollection   ' 置換語句集
    Private m_finderdata As StringCollection        ' 検索ダイアログの位置

メインフォーム側の検索・置換メニューの実装
    ' 編集メニュー : 検索
    Private Sub mnFind_Click(ByVal sender As System.Object, _
                    ByVal e As System.EventArgs) Handles mnFind.Click
        If Not IsNothing(TC1.SelectedTab) Then
            ' 検索・置換語句がないときは作成する
            If IsNothing(m_findwords) Then
                m_findwords = New StringCollection()
            End If
            If IsNothing(m_replacewords) Then 
                m_replacewords = New StringCollection()
            End If

            ' ダイアログを開いて参照データを送る
            Dim dlgFind As New FindDialog(Me, FindDialog.LoadPoint, FindDialog.MatchCase)
            dlgFind.SetFindData(TC1, m_findwords, m_replacewords)
            dlgFind.Show()
        End If
    End Sub

ダイアログ側のコード
    ' SetFindData : 検索データの設定
    ' ... 引数 tabctr : 対象のタブコントロール
    ' ... 引数 words1 : 検索語句のリスト
    ' ... 引数 words2 : 置換語句のリスト
    Public Sub SetFindData(ByRef tabctr As TabControl, _
                ByRef words1 As StringCollection, ByRef words2 As StringCollection)
        m_tabs = tabctr
        m_edit = DirectCast(m_tabs.SelectedTab, TextPage).EditBox
        m_findwords = words1
        m_replacewords = words2

        Dim elem As String
        If m_findwords.Count > 0 Then
            For Each elem In m_findwords
                ' 検索コンボボックスにデータを追加
                cbFind.Items.Add(elem)
            Next
        End If
        If m_replacewords.Count > 0 Then
            For Each elem In m_replacewords
                ' 置換コンボボックスにデータを追加
                cbReplace.Items.Add(elem)
            Next
        End If
    End Sub

検索・置換の実行コード

 検索・置換の対象となるのは、タブコントロールのアクティブなページです。選択した文字列があれば、その範囲内のみを検索・置換し、ないときはキャレットの位置から文書の最後までが範囲となります。

' [次を検索] ボタン
Private Sub bnFind_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
            Handles bnFind.Click
    Try
        Dim nStart As Integer = 1    ' 検索開始位置(初期値1=文書の先頭文字)
        ' 比較方法(バイナリモードかテキストモードか)
        Dim nCompare As CompareMethod _
        = CType(IIf(chkMatchcase.Checked, vbBinaryCompare, vbTextCompare), CompareMethod)
 
        ' 選択文字列がある場合は検索開始位置を再指定
        m_edit = DirectCast(m_tabs.SelectedTab, TextPage).EditBox
        If StrComp(m_edit.SelectedText, cbFind.Text, nCompare) = 0 Then
            nStart = m_edit.SelectionStart + m_edit.SelectionLength
        ElseIf m_edit.SelectionStart > 0 Then
            nStart = m_edit.SelectionStart
        End If

        ' 検索実行・・・見つかったらその位置へ移動
        Dim nHit As Integer = InStr(nStart, m_edit.Text, cbFind.Text, nCompare)
        If nHit > 0 Then
            With m_edit
                .Focus()
                .Select(nHit - 1, cbFind.Text.Length)
                .ScrollToCaret()
            End With
        Else
            ' 見つからなくなった時点で検索を終了させる
            Dim msg As String = "検索が終了しました。"
            MessageBox.Show(msg, "検索/置換", _
                    MessageBoxButtons.OK, MessageBoxIcon.Information)
        End If

        ' 検索語句リストの保守
        UpdateWordsList(cbFind, m_findwords)

    Catch ex As Exception
        MessageBox.Show(ex.Message, "検索エラー", _
                    MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
End Sub

' [置換検索] ボタン
Private Sub bnReplace_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
            Handles bnReplace.Click
    Try
        ' 検索語句と置換語句を入れ替える
        m_edit = DirectCast(m_tabs.SelectedTab, TextPage).EditBox
        If m_edit.SelectedText = cbFind.Text Then
            m_edit.SelectedText = cbReplace.Text
            m_edit.SelectionStart = m_edit.SelectionStart + cbReplace.Text.Length
        End If

        ' 次の語句を検索
        bnFind.PerformClick()
        ' 置換語句のリストの保守
        UpdateWordsList(cbReplace, m_replacewords)

    Catch ex As Exception
        MessageBox.Show(ex.Message, "置換検索エラー", _
                MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
End Sub

' [すべて置換] ボタン
Private Sub bnReplaceAll_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
            Handles bnReplaceAll.Click
    Try
        m_edit = DirectCast(m_tabs.SelectedTab, TextPage).EditBox
        ' 開始位置、長さ、終了位置の取得
        Dim m_start As Integer = 1
        Dim m_length As Integer = 0
        Dim m_end As Integer = m_edit.TextLength

        If m_edit.SelectionLength > 0 Then
            ' 選択文字列があるときは各位置を再設定
            m_start = m_edit.SelectionStart
            m_length = m_edit.SelectionLength
            m_end = m_edit.SelectionStart + m_edit.SelectionLength
        End If

        ' 置換の実行
        Dim nStart As Integer = m_start    ' 次の検索開始位置
        Dim nHit As Integer = -1        ' 見つかった位置
        Dim nCount As Integer = 0     ' 置換した数
        Dim nBalance As Integer = _
                cbReplace.Text.Length - cbFind.Text.Length  ' 置換語句の長さー検索語句の長さ
        Dim nCompare As CompareMethod _
        = CType(IIf(chkMatchcase.Checked, vbBinaryCompare, vbTextCompare), CompareMethod)
        Do
            ' ヒットした位置がなくなるまでループ
            nHit = InStr(nStart, m_edit.Text, cbFind.Text, nCompare)
            If nHit < 1 OrElse nHit > m_end Then
                Exit Do
            Else
                m_edit.Select(nHit - 1, cbFind.Text.Length)
                m_edit.SelectedText = cbReplace.Text
                nStart = nHit + cbReplace.Text.Length    ' 次の開始位置
                m_end += nBalance    ' 検索終了位置の修正
                nCount += 1             ' 置換した数の合計
            End If
        Loop

        ' 置換後の処理
        Dim msg As String = nCount & "個の置換が完了しました。"
        MessageBox.Show(msg, "すべて置換", _
                MessageBoxButtons.OK, MessageBoxIcon.Information)
        If nCount > 0 Then
            m_edit.Modified = True
            If m_length > 0 Then m_length += (nBalance * nCount)
        End If
        m_edit.Select(m_start, m_length)      ' 選択語句を復元
        m_edit.Focus()
        UpdateWordsList(cbFind, m_findwords)      ' 検索語句リストの保守
        UpdateWordsList(cbReplace, m_replacewords)  ' 置換語句リストの保守

    Catch ex As Exception
        MessageBox.Show(ex.Message, "置換エラー", _
                MessageBoxButtons.OK, MessageBoxIcon.Error)
    End Try
End Sub

' [閉じる] ボタン
Private Sub bnClose_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
            Handles bnClose.Click
    Me.Close()
End Sub
| ■HOME | ◆プログラムTop | ▲ページの先頭 | << 前の章 | 次の章 >> |