CEditView 雑記

Last Updated: Dec.22,2006
使用しているコンパイラは、Microsoft Visual C++ 6.0 です。これ以外のバージョンでは記述と食い違うことがあるかも知れません。

4. マルチレベルの Undo/Redo の実装

現在、世に出ているエディタ系のソフトでは当たり前のようにマルチレベルの Undo/Redo がサポートされていますが、それに比して、MFC の CEditView で実装されている Undo 機能は大変貧弱なものです(Windows のエディットコントロールがそのまま使われているだけなので仕方ないのですが)。そこで、CEditView にマルチレベルの Undo/Redo を実装する方法について考えてみます。

Undo/Redo の動作

一口に Undo/Redo とは言いますが、いったいどういったものなのか、どういった動作をすべきなのかについて図に示しながら考えてみようと思います。図内の ope は operation の略で通常の編集操作(文字あるいは文字列の挿入・削除・置換)を指します。

新しい編集操作を追加
ope ope ope ope new   ポインタの位置は新しい編集操作の挿入位置を指している
← undo
ope ope ope ope ope   ポインタの左側は Undo になる
← undo
Undo を3回実行
ope ope ope ope ope   ポインタを3つ左側に移動させる。ポインタ位置を含め、右側は Redo になる
← undo redo →  
Redo を1回実行
ope ope ope ope ope   ポインタを1つ右側に移動させる。
← undo redo →  
新しい編集操作を追加
ope ope ope new del   新しい編集操作が追加される場合は、挿入位置から右側の情報は不要なので削除する
← undo
ope ope ope ope      
← undo

これはあくまでも一例です。もっと高機能なものもあると思います。まあ、一般的にエディタに実装されているのはだいたいこういった感じだと思うので、これでやってみたいと思います。

* 以下のコードは _UNICODE ビルドを前提にしています。_MBCS ビルドでは日本語文字(2バイト文字)に対する処理が必要になります。

クラスの設計

CEditView の場合、1回の Undo/Redo に最低限必要なデータは、

  1. 文字列あるいは文字の挿入・削除・置換を示す値 → m_nOpe
  2. カレットの位置(選択されている場合は始点)→ m_nStart
  3. 挿入される文字列あるいは文字 → m_sInsert
  4. 削除される文字列あるいは文字 → m_sDelete

です。これをふまえて、Undo/Redo バッファ CUndoItem を定義してみます。

class CUndoItem {
public:
    int m_nOpe;
    int m_nStart;
    CString m_sInsert;
    CString m_sDelete;
    ...
};

次に、実際に CEditView に持たせるときのための Undo/Redo バッファの配列を定義します。

class CUndoArray : public CArray <CUndoItem,CUndoItem&>
{
public:
    CUndoArray();
    void Register(CEditView* pView);
    ...
protected:
    CEditView* m_pEditView;
    int m_nPos; /* バッファの現在位置 */
    BOOL m_bLock; /* バッファ操作中フラグ */
    ...
};

m_nPos は、次の編集操作を挿入する位置を指します。また、CEditView の操作が必要になりますので、Register 関数を用意して登録できるようにしています。CEditView の派生クラスの OnCreate で1回実行するだけです。

バッファを操作する関数

まず、文字列の挿入・削除・置換操作が行われた際にバッファの配列にデータを挿入するためのメンバ関数を定義します。

// 文字列の挿入・削除・置換
void CUndoArray::OP(int nID, LPCTSTR szText, int nStart, int nEnd)
{
    if (m_bLock) return;
    if (nID == UNDO_INS_KEY)
    {
        if (m_nPos > 0)
        {
            CUndoItem& pre = ElementAt(m_nPos - 1);
            if (pre.m_nOpe == UNDO_INSERT && szText != NULL &&
                nStart == pre.m_nStart + pre.m_sInsert.GetLength()) 
            {
                pre.m_sInsert += szText;
                return;
            }
        }
        nID = UNDO_INSERT;
    }
    if (nStart == -1)
        m_pEditView->GetEditCtrl().GetSel(nStart, nEnd);
    CUndoItem undo;
    undo.m_nOpe = nID;
    undo.m_nStart = nStart;
    if (szText != NULL)
        undo.m_sInsert = szText;
    if (nStart < nEnd)
        undo.m_sDelete = GetTextRange(nStart, nEnd - nStart);
    ClearUpper();
    InsertAt(m_nPos++, undo);
}

コード中の GetTextRange 関数は、選択範囲の文字列を取得するもの、ClearUpper 関数は、データを挿入したときに、それより右側の不要なデータを削除するものです。次に Undo/Redo を行う関数を定義します。

// Undo 実行
void CUndoArray::Undo()
{
    if (!CanUndo()) return;
    m_bLock = TRUE;
    CUndoItem& undo = ElementAt(--m_nPos);
    CEdit& edit = m_pEditView->GetEditCtrl();
    int nLen = undo.m_sInsert.GetLength();
    edit.SetSel(undo.m_nStart, undo.m_nStart + nLen);
    edit.ReplaceSel(undo.m_sDelete, TRUE);
    if (undo.m_nOpe == UNDO_DEL_KEY)
        edit.SetSel(undo.m_nStart, undo.m_nStart);
    m_bLock = FALSE;
    if (!CanUndo()) edit.EmptyUndoBuffer();
}

// Redo 実行
void CUndoArray::Redo()
{
    if (!CanRedo()) return;
    m_bLock = TRUE;
    CUndoItem& undo = ElementAt(m_nPos++); 
    CEdit& edit = m_pEditView->GetEditCtrl();
    int nLen = undo.m_sDelete.GetLength();
    edit.SetSel(undo.m_nStart, undo.m_nStart + nLen);
    edit.ReplaceSel(undo.m_sInsert, TRUE);
    m_bLock = FALSE;
}

どこでバッファを操作するか

自分でエディットクラスを設計する場合と違い、既存の CEditView に実装するわけですから、どこで Undo/Redo バッファを操作するかが重要になります。だいたい、以下の場合についてコーディングすることになります。

  1. 文字(キー)の入力(OnKeyDown, OnChar)が行われた
  2. クリップボード操作(OnEditCut, OnEditPaste, OnEditClear)が行われた
  3. コンテキストメニューから「元に戻す」、「切り取り」、「貼り付け」、「削除」が行われた
  4. 文字列の置換操作(OnEditReplace)が行われた
  5. アプリケーションで、ReplaceSel や Clear を使用した

2 番目以降については、ひとつひとつコマンドを実装していくよりも、メッセージをトラップして処理する方がすっきりと出来そうです。ただし、CUndoArray の操作中とそうでない場合を区別するためのフラグ(CUndoArray::m_bLock)が必要になります。

メニュー 送られるメッセージ ID Parameters
MFC コンテキスト
元に戻す EM_UNDO WM_UNDO Not used; must be zero.
切り取り WM_CUT Not used; must be zero.
貼り付け WM_PASTE Not used; must be zero.
削除(消去) WM_CLEAR Not used; must be zero.
置換 EM_REPLACESEL - wParam:Specifies whether the replacement operation can be undone. If this is TRUE, the operation can be undone. If this is FALSE , the operation cannot be undone.
lParam:Pointer to a null-terminated string containing the replacement text.

コーディング例

例によって、CEditView の派生クラス CMyView に、CUndoArray 型のメンバ m_arrayUndo を追加します。最初にすることは、OnCreate で、CUndoArray::Register を呼び出すことです。

int CMyView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CEditView::OnCreate(lpCreateStruct) == -1)
        return -1;

    m_arrayUndo.Register(this);

    return 0;
}

文字(キー)入力の捕捉は OnKeyDown と OnChar で行います。「7. Ctrl や Shift 押下時のキー入力について」で調査したことを踏まえて、まず、OnKeyDown では VK_DELETE を、OnChar では VK_BACK を捕まえて、 m_arrayUndo にキーコードを送ります。CUndoArray::DeleteChar はキーコードを受け取って、カレット位置の手前か、あるいは直後の文字位置を取得して、CUndoArray::OP を呼び出すようになっています。ここで注意することは、もし選択文字列があるときに、これらのキーが押されると選択文字列の削除しか行わないことです。

void CUndoEditView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    if (nChar == VK_DELETE)
        m_arrayUndo.DeleteChar(nChar);

    CEditView::OnKeyDown(nChar, nRepCnt, nFlags);
}

OnChar での処理は以下のようになります。なお、OnChar の第 2 引数 nRepCnt(キーを押し続けている間に発生するキーストロークの回数)に対する処理も行っています。

void CUndoEditView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    static UINT nPreChar = 0;
    int ss, se;
    UINT i;
    CString str;
    GetEditCtrl().GetSel(ss, se);
    if (nChar == VK_RETURN || nChar == 0x0A)
    {
        if (ss != se) /* 選択文字列あり */
            m_arrayUndo.OP(UNDO_REPLACE, _T("\r\n"), ss, se);
        else
        {
           for (i = 0; i < nRepCnt; i++)
               str += _T("\r\n");
           m_arrayUndo.OP(UNDO_INSERT, str, ss, se);
        }
    }
    else if (nChar == VK_BACK)
    {
        m_arrayUndo.DeleteChar(nChar, ss, se, nRepCnt);
    }
    else if (nChar >= 0x20 || nChar == VK_TAB)
    {
        TCHAR szText[2];
        wsprintf(szText, _T("%c"), (TCHAR)nChar);
        BOOL bAdd = ((nPreChar != VK_TAB && nChar != VK_TAB &&
            nPreChar != 0x20 && nChar != 0x20 && nPreChar != VK_RETURN) ||
            (nPreChar == 0x20 && nChar == 0x20));
        if (ss != se) /* 選択文字列あり */
            m_arrayUndo.OP(UNDO_REPLACE, szText, ss, se);
        else
        {
            for (i = 0; i < nRepCnt; i++)
               str += szText;
            m_arrayUndo.OP((bAdd ? UNDO_INS_KEY : UNDO_INSERT), str, ss, se);
        }
    }
    nPreChar = nChar;

    CEditView::OnChar(nChar, nRepCnt, nFlags);
}

あとは、OnEditUndo, OnEditRedo コマンドでそれぞれ、m_arrayUndo.Undo, m_arrayUndo.Redo を呼び出すようにすれば、とりあえず実行できるようになります。

←前ページ 次ページ→

Back Copyright (c) 2003, 2006 AOK. All Rights Reserved.