ホーム | Software Lab | Profile | Favorites | Diary | Links | About This Site | Feedback

 

 

第8回は、 ソース表示の基礎となる View クラスを実装し、簡単な変換クラス(ソースとHTML)を設計します。

今回のポイント
RichEdit を使ってみよう

  •  CSourceView クラスの実装

    それでは、早速 Source を表示する View を作成していきます。

    今回は、テキストをキーワードごとに着色することも考えて RichEditCtrl を使用します。定義は、AtlCtrls.h にあります。そのまま CSplitterWindow に割り当ててもいいのですが、ファイルのロードとセーブにストリームを使ったコールバックを使ってみたかったのと、その辺の記述を MainFrame に記述したくないこともあります。

    こうすることのメリットには、CRichEditCtrl の各メソッドを使った一連の流れを、MainFrm に書くよりは、一連の処理を CRichEditCtrl のほうにメソッドとして用意しておいて、MainFrm からはそのメソッドを呼ぶだけで済むように記述できる点が挙げられると思います。

    では、早速 RichEditCtrl を使っていきます。RichEditCtrl はコモンコントロールとは別の dll になっているので、使う前に dll をロードしてやる必要があります。これは、WTL でこのような作業を行う際にはほとんど定型処理のようなものなので、この機会に覚えてしまいましょう。

    プロジェクトのメインソース(<プロジェクト名>.CPP)にロードするコードを書きます(今回は S2H_Conv.cpp)

    int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)
    {
        HRESULT hRes = ::CoInitialize(NULL);
        // If you are running on NT 4.0 or higher you can use the following call instead to 
        // make the EXE free threaded. This means that calls come in on a random RPC thread.
        // HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
        ATLASSERT(SUCCEEDED(hRes));
    
        // this resolves ATL window thunking problem when Microsoft Layer for Unicode (MSLU) is used
        ::DefWindowProc(NULL, 0, 0, 0L);
    
        AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES); // add flags to support other controls
    
        hRes = _Module.Init(NULL, hInstance);
        ATLASSERT(SUCCEEDED(hRes));
    
        /// リッチエディットをロード
        HINSTANCE hInstRich = ::LoadLibrary(CRichEditCtrl::GetLibraryName());
    
        int nRet = Run(lpstrCmdLine, nCmdShow);
    
        ::FreeLibrary(hInstRich);
    
        _Module.Term();
        ::CoUninitialize();
    
        return nRet;
    }

    上記は S2H_Conv.cpp の開始部分ですが、Run で始まる部分がメインスレッド(要はプログラム本体のようなもの)になります。ですから、この前にライブラリをロードし、この後でライブラリを開放します。ロードには LoadLibrary、開放には FreeLibrary という API を使います。詳しくは MSDN を見てください。

    次に、RichEdit を使った View を作成します。とりあえずここでは、CSourceView としました。Wizard で作成したときに作られた、S2H_ConvView のヘッダファイルとソースファイルをそれぞれリネームしてプロジェクトに読み込みます。大きな変化があるのはクラス宣言の部分です。

    class CSourceView : public CWindowImpl<CSourceView, CRichEditCtrl, CSourceViewWinTraits>
    {
    public:
        DECLARE_WND_SUPERCLASS(NULL, CRichEditCtrl::GetWndClassName())
    
    

    CSourceView も CWindowImpl を継承するのですが、2つ目のテンプレート引数の TBase、つまりベースとなるクラスには CWindow ではなく、CRichEditCtrl を指定します。また、RichEditCtrl に指定するウィンドウスタイルは、CWinTraits を使って指定し、3つ目のテンプレート引数に指定します。

    また、クラスの最初に DECLARE_WND_SUPERCLASS というマクロが使われています。ATL ではデフォルトでは DECLARE_WND_CLASS() で指定された新しいクラスに基づくウィンドウを生成しますが、DECLARE_WND_SUPERCLASS マクロの場合は、2番目の引数で指定されたクラスに基づく(つまり今回の場合は CRichEditCtrl) 新しい名前のウィンドウを作成することになります。

    ただし、DECLARE_WND_SUPERCLASS の最初の引数に NULL を指定した場合は ATL***** という名前を勝手につけられます。自分の好みのウィンドウ名にしたい場合は名前を指定します。DECLARE_WND_SUPERCLASS では、RichEditCtrl に基づいたウィンドウを作成しますが、ウィンドウプロシージャは RichEdit のものではなく、独自のものを利用します。

  •  Stream コールバックを利用した読み込みと保存の実装

    どちらかというと、SDK 的な内容で特に WTL というわけではないのですが、せっかくですから RichiEdit の Stream を利用してみましょう。

    Stream を使う利点にはいくつかありますが、RTF 形式で保存できたり、コールバックを使って読み込み書き込みのコントロールをすることができます。たとえば、何バイト転送されてきたとか、その間にプログレスバーを表示させて経過を表示させるといったことができます。

    また、CPU やメモリと比較して DISK への I/O はコストが大きいので、CPU が遊んでしまう可能性があります。Stream ではこある程度ずつ読み込むことができるので、読み込みながら構文解析とか、何らかの処理を行って CPU を活用するということが考えられます。

    ということで、読み込み側は以下のようになります。

    BOOL CSourceView::LoadFile(BOOL bShowDialog, LPTSTR lpstrFilePath)
    {
        CString sPath(lpstrFilePath);
    
        if ( FALSE == bShowDialog ) {
            /// ファイル名が決められない場合はエラー
            ATLASSERT(sPath.IsEmpty() != FALSE);
        } else {
            /// 「ファイルを開く」ダイアログを表示
            CFileDialog cFileDialog(TRUE, m_sDefaultExt);
    
            if ( IDOK == cFileDialog.DoModal()) {
                sPath = cFileDialog.m_szFileName;
            }
        }
    
        if ( sPath.IsEmpty()) {
            MessageBox(_T("ファイル名が指定されませんでした。"), _T("エラー"), MB_OK | MB_ICONEXCLAMATION);
            return FALSE;
        }
    
        HANDLE hFile = ::CreateFile(lpstrFilePath, GENERIC_READ, 0, NULL, OPEN_EXISTING,
                                                    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    
        if( hFile == INVALID_HANDLE_VALUE )
            return FALSE;
    
        /// ストリームを使って読み込む
        EDITSTREAM es;
        es.dwCookie = (DWORD)hFile;
        es.dwError = 0;
        es.pfnCallback = StreamReadCallback;
        StreamIn(SF_TEXT, es);
    
        ::CloseHandle(hFile);
    
        return !(BOOL)es.dwError;
    }
    
    DWORD CALLBACK CSourceView::StreamReadCallback(DWORD dwCookie, LPBYTE pbBuff, LONG cb, LONG FAR *pcb)
    {
        ATLASSERT(dwCookie != 0);
        ATLASSERT(pcb != NULL);
    
        return !::ReadFile((HANDLE)dwCookie, pbBuff, cb, (LPDWORD)pcb, NULL);
    }

    外部(この場合は CMainFrame)から操作することを考えて、読み込み操作は LoadFile というメソッドにしました。 CMainFrame とこのメソッドの内のどちらでコモンダイアログに対応するか迷いましたが、CMainFrame では二つの View に操作を割り当てる部分があるので、今回は View でコモンダイアログを制御するようにしてみました。

    コールバック関数は、MSDN の EditStreamCallback という項目を調べていただければわかるのですが、static で宣言してあることに注意してください。指定されたものとまったく同じ引数をもち、任意の名前をつけることが可能です。

    同様に書き込みのほうも実装します。

    BOOL CSourceView::SaveFile(BOOL bShowDialog, LPTSTR lpstrFilePath)
    {
        CString sPath(lpstrFilePath);
    
        if ( FALSE == bShowDialog ) {
            /// ファイル名が決められないのでエラー
            ATLASSERT(sPath.IsEmpty() != FALSE || m_sDefaultFile.IsEmpty() != FALSE);
        } else {
            /// 「名前を付けて保存」のダイアログを表示
           CFileDialog cFileDialog(FALSE, m_sDefaultExt, m_sDefaultFile);
    
            if ( IDOK == cFileDialog.DoModal()) {
                sPath = cFileDialog.m_szFileName;
            }
        }
    
        HANDLE hFile = ::CreateFile(sPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                                                    FILE_ATTRIBUTE_ARCHIVE | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    
        if(hFile == INVALID_HANDLE_VALUE) {
            MessageBox(_T("ファイルの作成に失敗しました。"), _T("エラー"), MB_OK | MB_ICONERROR);
    
            return FALSE;
        }
    
        EDITSTREAM es;
        es.dwCookie = (DWORD)hFile;
        es.dwError = 0;
        es.pfnCallback = StreamWriteCallback;
        StreamOut(SF_TEXT, es);
    
        ::CloseHandle(hFile);
    
        return !(BOOL)es.dwError;
    }
    
    DWORD CALLBACK CSourceView::StreamWriteCallback(DWORD dwCookie, LPBYTE pbBuff, LONG cb, LONG FAR *pcb)
    {
        ATLASSERT(dwCookie != 0);
        ATLASSERT(pcb != NULL);
    
        return !::WriteFile((HANDLE)dwCookie, pbBuff, cb, (LPDWORD)pcb, NULL);
    }

    読み込みのときも書き込みのときも、Stream への情報のセットには EDITSTREAM 構造体を利用します。また、StreamIn, StreamOut という関数が実際の読み書き開始のトリガーになります。Stream Callback 関数は任意の回数呼ばれますので、そのつど読み込みの場合は ReaFile API、書き込みの際は WriteFile API を呼んで対処します。

    CFileDialog はコモンダイアログの特にファイルの読み書きに特化したラッパークラスです。コンストラクタで作成時の1つ目の引数に TRUE を設定すれば「ファイルを開く」ダイアログ、FALSE を指定すれば「名前をつけて保存」ダイアログが表示されます。

    その他、デフォルトのファイル名を決めたり、CEdit のすべて削除(SetSel で全選択して、Clear)といった操作のラッパーメソッドなどを実装しています。詳しくはソースを読んでください。

  •  変換クラスの設計

  • 変換クラスの役割は以下のようにまとめられると思います。

    •  変換に関する情報(キーワード、色、コメントの扱いなど) の設定と取得

    •  実際の変換ルーチン(これがキモ)

    •  変換対象のテキストの設定と、変換後のテキストの取得

    変換の対象ソースは C++、VB、Perl、Ruby、Java・・・ といったようにいくつでも考えられますので(ここですべてを実装するわけではありません)、対象テキストの設定と取得というメソッドを除いて共通の内容はありません。しかし、その作業を指示する方法(インタフェース)は共通です。

    つまり何の言語であろうと、変換に必要な情報をセットして、テキストをセットして変換して、結果を取得するという流れには何の変わりもありません。ということで、IConvertor という変換クラスに共通なインタフェースを定義します。

    class IConvert 
    {
    public:
        enum CommentType {
            LineComment = 0, // 行コメント
            BlockComment = 1, // ブロックコメント
        };
    
        IConvert();
        virtual ~IConvert();
    
        virtual BOOL SetText(const CString& sSource) = 0;
        virtual BOOL GetText(CString& sOut) = 0;
        virtual BOOL Convert() = 0;
    
        virtual void SetCommentColor(COLORREF col) = 0;
        virtual void SetKeywordColor(COLORREF col) = 0;
        virtual void SetDefaultColor(COLORREF col) = 0;
    
        virtual BOOL SetKeyword(LPCTSTR sKeyword) = 0;
        virtual BOOL DeleteKeyword(LPCTSTR sKeyword) = 0;
        virtual BOOL DeleteAllKeyword() = 0;
        virtual LONG GetRowCount() = 0;
        virtual BOOL SetCommentType(CommentType t, LPCTSTR sCommentStart, LPCTSTR sCommentEnd = _T("") ) = 0;
    
    protected:
        const static LPCTSTR m_sHeader;
        const static LPCTSTR m_sFooter;
    };

    きちんと設計してないので(汗)今後変更する可能性がありますが、コメントの扱いは複数のパターンが考えられること、デストラクタは仮想デストラクタにしておくこと(継承が前提なので)、すべてのクラスに必要なインタフェースの部分は純粋仮想関数にして、実装を強制すること。それから、これは必要か微妙ですが(変換後の対象が HTML に限定されるので)、HTML のヘッダとフッタの部分を固定文字列にしてみました。

    そして、これを実装したクラスを作っていくわけですが、この講座では C++ への変換クラス部分を中心に進めていきます(僕が欲しいから)。また、ユーザーが拡張しやすいように、他の言語での変換が簡単になるよう、動的に変換クラスを切り替えられるように、実装します。

    それでは、C++ 変換クラス、名づけて Cpp2Html の実装です(本当にそのまんまだな・・・)

    class CCpp2Html : public IConvert
    {
    public:
        CCpp2Html();
        virtual ~CCpp2Html();
    
    
        virtual BOOL SetText(const CString& sSource);
        virtual BOOL GetText(CString& sOut);
        virtual BOOL Convert();
    
        virtual void SetCommentColor(COLORREF col);
        virtual void SetKeywordColor(COLORREF col);
        virtual void SetDefaultColor(COLORREF col);
    
        virtual BOOL SetKeyword(LPCTSTR sKeyword);
        virtual BOOL DeleteKeyword(LPCTSTR sKeyword);
        virtual BOOL DeleteAllKeyword();
        virtual LONG GetRowCount();
        virtual BOOL SetCommentType(CommentType t, LPCTSTR sCommentStart, LPCTSTR sCommentEnd = _T("") );
    
    private:
        COLORREF colComment; /// コメントの色
        COLORREF colDefault; /// デフォルトの色
        COLORREF colKeyword; /// キーワードの色
    
        CString m_sOrigText; /// 変換対象元
        CString m_sText; /// 変換結果
    };

    キモになる部分は Convert() の実装ですが、これは次回以降でやります。メンバ変数として、色情報と変換前と変換後の文字列をとりあえず持ってみました。

  •  CMainFrame への実装

    それではここまでの成果を CMainFrame に実装していきます。

    まず、二つの View は CSourceView のインスタンスとして作ります(今後変わる可能性があります)。また、変換作業をする関数と変換に使う IConverter を返す関数を実装します。

    class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>,
    public CMessageFilter, public CIdleHandler
    {
    public:
        DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)
    
        CMainFrame();
        ~CMainFrame();
    
        CFolderView m_cFolderView;
        CSourceView m_cSourceView;
        CSourceView m_cHtmlSourceView;
    
    	・
    	略
    	・
        IConvert* GetConverter();	/// 変換クラスを返すprivate:
        void Translate(BOOL bAll = TRUE); /// 実際の変換を行う
    }

    GetConvertor 関数は IConvertor インタフェースへのポインタを返します。つまり、変換クラスを操作する方法を知っているというわけです。どの変換方法を使うかは、のちのち実装しますが、メニューから変換方法を選び、それをメンバ変数に格納し、その変数の内容によって、変換する方法を使い分けるということになります。

    IConvert* CMainFrame::GetConverter()
    {
        /// コンバーターは必要になったら自分で IConvert を継承して変換クラスを作ってみてください
        switch ( m_nConvMode ) {
        case 0:
            /// Cpp 用
            return (new CCpp2Html);
    
            break;
        case 1:
            /// Java 用
    
           break;
        case 2:
            /// VB 用
    
            break; 
        default:
           break;
        }
    
        return NULL;
    }

    ここでは現在のモードに従って対応するクラスを返します。返すときには new を使ってインスタンスを生成しているので、使い終わったら delete する必要があることに注意してください。また、任意の変換クラスを追加することができます。IConvert を継承した変換クラス(たとえば、CJava2Html) などを設計、実装して使い分けることを想定しています。

    実際に変換する部分は以下のようになります。

    void CMainFrame::Translate(BOOL bAll)
    {
        IConvert* pIConvert;
    
        /// 現在選択されている変換ルーチンを読み込む
        pIConvert = GetConverter();
    
        if ( pIConvert ) {
            CString sText;
    
            CHARRANGE cr;
            m_cSourceView.GetSel(cr);
    
            long lLen = 0;
    
            /// 変換範囲を決定
            if ( bAll || ( 0 == cr.cpMin && 0 == cr.cpMax || 0 == cr.cpMin && -1 == cr.cpMax )) {
                /// 選択範囲がない場合と、全選択の場合は全テキストを対象とする
                lLen = m_cSourceView.GetTextLength() + 2; // CR, LF の分
    
                if ( 0 < lLen ) {
                    m_cSourceView.GetWindowText(sText.GetBuffer(lLen), lLen);
                    sText.ReleaseBuffer();
                } else {
                    MessageBox(_T("対象ソースがありません。"), _T("警告"), MB_OK | MB_ICONEXCLAMATION);
                }
            } else {
                lLen = cr.cpMax - cr.cpMin + 2; // CR, LF の分
    
                m_cSourceView.GetSelText(sText.GetBuffer(lLen));
    
                /// ReleaseBuffer しないと、sText が上記 GetSelText で
                /// 自分のバッファが書き換えられたことがわからない
                sText.ReleaseBuffer();
            }
    
            /// 変換対象テキストがある場合のみ変換作業開始
            if ( FALSE == sText.IsEmpty()) {
                /// 対象テキストをセット
                pIConvert->SetText(sText);
    
                /// 変換
                if ( TRUE == pIConvert->Convert() ) {
                   /// 変換結果をゲット
                   pIConvert->GetText(sText);
    
                   /// Source View に表示
                   m_cHtmlSourceView.SetWindowText(sText);
                } else {
                    MessageBox(_T("変換に失敗しました。"), _T("エラー"), MB_OK | MB_ICONERROR);
                }
            }
    
            /// GetConverter() で獲得(内部的には new)したので解放
            delete pIConvert;
        }
    }

    動的に変換エンジンを切り替えられるあたりが、IConverter を使った設計の利点ですね。それから、SourceView にテキストがある場合、選択範囲があれば選択範囲のみを、選択範囲がなければすべてを変換するように作りました。

    実際の変換エンジンの中身は実装していないので、Pre タグで囲まれるだけですが、このページのように黄色の四角で囲まれるhtml に変換します。変換結果を表示するビューア部分をどうするか決めていないので、とりあえず、保存したいビューにフォーカスがある状態で保存ボタンを押して Html ファイルに保存して結果を確認してみてください。

    付属の Main.css というカスケードスタイルシートを利用していますので、このファイルがない場所に保存した場合は何のこともないテキストになってしまうのでご注意ください。

    ということで、非常に駆け足でしたがとりあえず動くファイルとコメントがある程度役に立つと思うので動かしていじって楽しんでみてください。

  • イベント周りの説明をしていませんでしたが、基本的に Edit のイベントを CMainFrm のメッセージマップで処理しています。SourceView、HtmlSourceView のどちらのイベントも同じハンドラで補足しますので、現在フォーカスのある View に処理を振り分けるように書きました。

  •  次回は・・・

    ちょっと急ぎ足でしたが、わからない部分やここをもっと説明して欲しいという部分は再度取り上げたり、この講座に補足する可能性がありますので、掲示板、メールなどにてご意見ください。操作感が微妙(使いづらい)なのはそのうち直していく予定です。

    ここまでの内容はこちらからダウンロードできます(117 KB)

    次回は RichEdit のキーワード部分の着色か、C++ 変換エンジンの実装部分をやりたいと思います。

    第9回

最終更新日 : 2005/04/13