チュートリアル④ ~辞書やコメントの情報を表示~

  • 概要

    前節では、入力補完の説明っぽい見た目のウィンドウを表示しました。
    今回は、辞書ファイルから読み込んだり、開いている「保存済み」のファイルを解析して表示してみましょう。

  • 辞書ファイルの用意

    「事前に用意した辞書」を読み込むプログラムとなりますので、
    辞書ファイルを作成しましょう。
    「HmAutoCompleteExPlug.dic」といった名前で「HmAutoCompleteExPlug.dll」と同じフォルダに保存することとなります。

    今回は、辞書ファイルは、cp932だと決め打ちましたのでcp932にて保存しましょう。
    (もちろん65001と書き換えてutf8にしてもかいまいませんが、その場合にはソースコードの修正も必要です。)

    print★私のprint解説◆改行しました◆また改行しました。
    pipe★私のpipe解説◆改行しました◆また改行しました。
    
  • 辞書の読み込みや、今開いている(保存済みの)ファイルの解析

    #include <windows.h>
    
    #include "OutputDebugStream.h"
    
    using namespace std;
    
    using namespace System;
    using namespace System::IO;
    
    using namespace System::Collections::Generic;
    
    using namespace System::Text;
    using namespace System::Text::RegularExpressions;
    
    using namespace System::Drawing;
    using namespace System::Windows::Forms;
    
    
    public ref class ACHelpForm : public Form
    {
    public:
      static ACHelpForm^ f; // 自分自身の置き場
    
    private:
      HWND hWnd;
      Label^ label;     // 説明文用のラベル
      int nTickRemainCnt;
      static Timer^ timer;
      static String^ filename; // 現在のファイル名
    
      static String^ dllFullPath;
      static String^ dicFullPath;
      static Dictionary<String^, String^>^ dic;
    public:
      ACHelpForm(String^ filename)
      {
        this->filename = filename;
    
        this->Visible = false;
        this->BackColor = System::Drawing::Color(::Color::White);
    
        SetFormAttr();
    
        label = gcnew Label();
        label->Top = 10;
        label->Left = 10;
    
        this->Controls->Add(label);
    
        // ディクショナリオブジェクト生成
        dic = gcnew Dictionary<String^, String^>();
        LoadDicFile();
        LoadCurFile();
      }
    
      void LoadDicFile() {
        dllFullPath = System::Reflection::Assembly::GetExecutingAssembly()->Location;
        // 自分自身のディクションナリ
        dicFullPath = Path::ChangeExtension(dllFullPath, ".dic");
    
        IO::StreamReader^ sr;
        try {
          sr = gcnew IO::StreamReader(dicFullPath, ::Encoding::GetEncoding(932)); // 説明文のファイルはsjis(cp932)と決め打った
    
          String^ line = "";
          // 読み込みできる文字がなくなるまで繰り返す
          while ((line = sr->ReadLine()) != nullptr) {
            Regex^ re = gcnew Regex(R"(^(.+?)★(.+)$)"); // キーと説明文の境界線は「★」にしておく
            Match^ m = re->Match(line);
            if (m->Success) {
              String^ keyword = m->Groups[1]->Value;
              String^ details = m->Groups[2]->Value;
              // キーも値も両方有効
              if (keyword && details) {
                details = details->Replace("◆", "\n"); // 「◆」は改行記号に変えておく
                dic[keyword] = details;
              }
            }
          }
    
        }
        catch (Exception^ e) {
          Windows::Forms::MessageBox::Show(e->Message);
        }
        finally {
          if (sr) {
            sr->Close();
          }
        }
      }
    
      void LoadCurFile() {
    
        if (IO::File::Exists(filename) == false) {
          return;
        }
    
        IO::StreamReader^ sr;
        try {
          sr = gcnew IO::StreamReader(filename, ::Encoding::GetEncoding(65001)); //.plはutf8の文字コードと決め打った場合
    
          String^ line = sr->ReadToEnd();
    
          Regex^ re = gcnew Regex("((^#.+?\n)+)sub\\s+([^\\s]+)", RegexOptions::Multiline); // 1行としてみなして
          MatchCollection^ mc = re->Matches(line);
    
          for each(Match^ m in mc) {
            GroupCollection^ groups = m->Groups;
            String^ keyword = groups[3]->Value;
            String^ details = groups[1]->Value;
            // キーも値も両方有効
            if (keyword && details) {
              dic[keyword] = details;
            }
          }
    
        }
        catch (Exception^) {
        }
        finally {
          if (sr) {
            sr->Close();
          }
        }
      }
    
    
    public:
      void Update(HWND hWnd, int iListBoxSelectedIndex, String^ strListBoxSelectedItem, int iItemHeight)
      {
        this->hWnd = hWnd;
        this->SuspendLayout();
        AdjustToAutoComp();
        // フォームのサイズや位置を再配置
    
        // それに応じてラベルのサイズが決まる
        label->Width = this->Width - label->Left * 2;
        label->Height = this->Height - label->Top * 2;
        label->Font = gcnew System::Drawing::Font("MS 明朝", (float)iItemHeight*0.7f); // 1.5倍ぐらい
    
        try {
          label->Text = dic[strListBoxSelectedItem];
        }
        catch (Exception^) {
          label->Text = "";
        }
        this->ResumeLayout();
    
        CreateDelayTimer();
      }
    
    protected:
      // 入力補完ウィンドウの右に、幅約2倍、高さ160pxへと自分自身を移動
      void AdjustToAutoComp()
      {
        RECT rect;
        GetWindowRect(this->hWnd, &rect);
        this->Left = rect.right + 24;
        this->Top = rect.top;
        this->Width = (rect.right - rect.left) * 2;
        this->Height = 160;
    
        DrawRectangle();
      }
    
      // フォームの外淵に沿って四角を描画
      void DrawRectangle()
      {
        Graphics^ g = this->CreateGraphics();
        Pen^ pen = gcnew Pen(::Color::Gray, 1);
        g->DrawRectangle(pen, 0, 0, this->Width - 1, this->Height - 1);
        delete pen;
        delete g;
      }
    
      // 入力補完などで状態が切り替わったら、6ミリx100回ぐらいはトライして、状態を追従し続ける。
      // 状態追従の安定化の一環
      void CreateDelayTimer()
      {
        if (timer) {
          timer->Stop();
        }
    
        timer = gcnew Timer();
        timer->Interval = 6;
        timer->Tick += gcnew EventHandler(this, &ACHelpForm::TimerTick);
        timer->Start();
        nTickRemainCnt = 100;
      }
    
      void TimerTick(Object^ sender, EventArgs^ e)
      {
        AdjustToAutoComp();
    
        if (label->Text->Length > 0) { // 単語が有効であれば
          this->Show();
        }
        else {
          this->Hide();
        }
    
        nTickRemainCnt--;
        if (nTickRemainCnt < 0) {
          timer->Stop();
        }
      }
    
    public:
      void StopTimer()
      {
        if (timer) {
          timer->Stop();
        }
      }
    
    protected:
      void SetFormAttr() {
        //タイトルバーを消す
        this->ControlBox = false;
        this->Text = "";
        this->FormBorderStyle = ::FormBorderStyle::None;
      }
    
      // フォーム表示時にアクティブにならないようにする
      property bool ShowWithoutActivation
      {
        virtual bool get() override
        {
          return true;
        }
      }
    
      // このフォームがクリックなどされた時にアクティブにならないようにする。
      virtual void WndProc(Message %m) override
      {
        if (m.Msg == WM_MOUSEACTIVATE)
        {
          m.Result = (IntPtr)MA_NOACTIVATE;
          return;
        }
    
        Form::WndProc(m);
      }
    
    };
    
    
    
    extern "C" __declspec(dllexport) int OnCreate(HWND hWnd, LPCTSTR szFileName) {
    
      // ウィンドウの作成
      if (ACHelpForm::f == nullptr || ACHelpForm::f->IsDisposed) {
        ACHelpForm::f = gcnew ACHelpForm(gcnew String(szFileName));
      }
    
      return TRUE;
    }
    
    extern "C" __declspec(dllexport) int OnListBoxSelectedIndexChanged(HWND hWnd, int iListBoxSelectedIndex, LPCTSTR szListBoxSelectedItem, int iItemHeight) {
    
      ACHelpForm::f->Update(hWnd, iListBoxSelectedIndex, gcnew String(szListBoxSelectedItem), iItemHeight);
    
      return TRUE;
    }
    
    
    extern "C" __declspec(dllexport) int OnDestroy(HWND hWnd) {
    
      // ウィンドウの破棄
      if (ACHelpForm::f) {
        ACHelpForm::f->StopTimer();
        ACHelpForm::f->Close();
      }
    
      // 明示的に解放
      if (ACHelpForm::f) {
        delete ACHelpForm::f;
      }
    
      return TRUE;
    
    }
    
  • 解説

    • 「LoadDicFile」では「cp932(sjis)」の辞書であると決め打って読み込んでいます。
      補完単語と説明文の境界を「★」として、改行マークを「◆」にすることで、パースを容易にしています。
    • 「LoadCurFile」では「.pl」ファイルは「utf8」と決め打って読み込んでいます。
      「#で始まる行が1行以上あり、続けてsub ○○○」といった形であれば、「関数へのコメントである」として分析しています。
      ………
        // ディクショナリオブジェクト生成
        dic = gcnew Dictionary<String^, String^>();
        LoadDicFile();
        LoadCurFile();
      }
    
      void LoadDicFile() {
        dllFullPath = System::Reflection::Assembly::GetExecutingAssembly()->Location;
        // 自分自身のディクションナリ
        dicFullPath = Path::ChangeExtension(dllFullPath, ".dic");
    
        IO::StreamReader^ sr;
        try {
          sr = gcnew IO::StreamReader(dicFullPath, ::Encoding::GetEncoding(932)); // 説明文のファイルはsjis(cp932)と決め打った
    
          String^ line = "";
          // 読み込みできる文字がなくなるまで繰り返す
          while ((line = sr->ReadLine()) != nullptr) {
            Regex^ re = gcnew Regex(R"(^(.+?)★(.+)$)"); // キーと説明文の境界線は「★」にしておく
            Match^ m = re->Match(line);
            if (m->Success) {
              String^ keyword = m->Groups[1]->Value;
              String^ details = m->Groups[2]->Value;
              // キーも値も両方有効
              if (keyword && details) {
                details = details->Replace("◆", "\n"); // 「◆」は改行記号に変えておく
                dic[keyword] = details;
              }
            }
          }
    
        }
        catch (Exception^ e) {
          Windows::Forms::MessageBox::Show(e->Message);
        }
        finally {
          if (sr) {
            sr->Close();
          }
        }
      }
    
      void LoadCurFile() {
    
        if (IO::File::Exists(filename) == false) {
          return;
        }
    
        IO::StreamReader^ sr;
        try {
          sr = gcnew IO::StreamReader(filename, ::Encoding::GetEncoding(65001)); //.plはutf8の文字コードと決め打った場合
    
          String^ line = sr->ReadToEnd();
    
          Regex^ re = gcnew Regex("((^#.+?\n)+)sub\\s+([^\\s]+)", RegexOptions::Multiline); // 1行としてみなして
          MatchCollection^ mc = re->Matches(line);
    
          for each(Match^ m in mc) {
            GroupCollection^ groups = m->Groups;
            String^ keyword = groups[3]->Value;
            String^ details = groups[1]->Value;
            // キーも値も両方有効
            if (keyword && details) {
              dic[keyword] = details;
            }
          }
    
        }
        catch (Exception^) {
        }
        finally {
          if (sr) {
            sr->Close();
          }
        }
      }
    
    
    
    
  • 再度コンパイル

    再度コンパイルし、「HmAutoCompleteExPlug.dll」を秀丸ディレクトリにコピーしましょう。

    再び、.plファイルを読み込んで、何か入力補完を出し、単語を選択してみましょう。
    以下のように、「辞書の説明文」は「編集中」のコメント説明文が表示されたでしょうか。