チュートリアル⑤ ~編集中のテキスト~

  • 概要

    前節では、辞書や保存済みのファイルを分析し、説明文として表示しました。
    しかし、本来表示するべきなのは、「保存済みのファイル」の分析結果ではなく、
    編集中のファイルの分析結果である、というのが理想です。

  • 秀丸 ver 8.66以降実現可能となった秀丸エディタで編集中データへの直接の問い合わせ

    秀丸エディタ ver 8.66からは編集中のデータをネイティブ層で直接取得することが可能となりました。
    又、カーソルの位置もネイティブ層から直接取得が可能となりました。

  • ファイルの分析→編集中のテキストの分析へと変更

    #include <windows.h>
    #include "OutputDebugStream.h"
    
    #include <msclr/marshal.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;
    
    using namespace msclr::interop;
    
    
    public ref class ACHelpForm : public Form
    {
    public:
      static ACHelpForm^ f; // 自分自身の置き場
    
    private:
      HWND hWnd;
      Label^ label;     // 説明文用のラベル
      int nTickRemainCnt;
      static Timer^ timer;
    
      static String^ dllFullPath;
      static String^ dicFullPath;
      static Dictionary<String^, String^>^ dic;
    
      // 秀丸本体からExportされている関数をセットする。
      static HMODULE hHidemaruExeInstance = GetModuleHandle(NULL);
      static HGLOBAL (WINAPI *GetTotalTextUnicode)(void)                      = (decltype(GetTotalTextUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetTotalTextUnicode");
      static HGLOBAL (WINAPI *GetLineTextUnicode)(int nLineNo)                = (decltype(GetLineTextUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetLineTextUnicode");
      static BOOL (WINAPI *GetCursorPosUnicode)(int* pnLineNo, int* pnColumn) = (decltype(GetCursorPosUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetCursorPosUnicode");
      static BOOL (WINAPI *CheckQueueStatus)(void)                            = (decltype(CheckQueueStatus))GetProcAddress(hHidemaruExeInstance, "Hidemaru_CheckQueueStatus");
    
    public:
      ACHelpForm()
      {
        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();
        LoadCurText();
      }
    
      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();
          }
        }
      }
    
      // 現在のカーソルのポジションを得る(1オリジン)
      Point^ GetCursorPos() {
        if (!GetCursorPosUnicode) {
          return gcnew Point(1, 1);
        }
    
        // ネイティブで秀丸エディタに問い合わせ
        int nLineNo = 0;
        int nColumn = 0;
        GetCursorPosUnicode(&nLineNo, &nColumn);
        // マネージドでPoint参照型にしておく
        Point^ p = gcnew Point(nLineNo, nColumn);
        return p;
      }
    
      void LoadCurText() {
        if (!GetTotalTextUnicode) {
          return;
        }
        Point^ p = GetCursorPos();
        OutputDebugStream(L"カーソル位置 x:%d, y:%d\n", p->X, p->Y);
    
        String^ all = "";
    
        // 現在編集中のテキストを得る
        HGLOBAL hGlobal = GetTotalTextUnicode();
        if (hGlobal) {
          // ネイティブでピンポインタ状態で得る
          wchar_t* pwsz = (wchar_t*)GlobalLock(hGlobal);
          // マネージドにコピー
          all = marshal_as<System::String^>(pwsz);
          // ネイティブは解放
          GlobalUnlock(hGlobal);
          GlobalFree(hGlobal);
        }
    
        try {
    
          Regex^ re = gcnew Regex("((^#.+?\n)+)sub\\s+([^\\s]+)", RegexOptions::Multiline); // 1行としてみなして
          MatchCollection^ mc = re->Matches(all);
    
          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^) {
        }
      }
    
    
    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();
      }
    
      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;
    
    }
    
  • 解説

    「LoadCurText」まわりが今回の注目点でしょう。
    コンストラクタにて、「Hidemaru.exe」本体からExportされている関数群をセットしています。

    セットした関数を利用して、「カーソルの位置」や「現在編集中のテキスト」を取得しています。

      // 秀丸本体からExportされている関数をセットする。
      static HMODULE hHidemaruExeInstance = GetModuleHandle(NULL);
      static HGLOBAL (WINAPI *GetTotalTextUnicode)(void)                      = (decltype(GetTotalTextUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetTotalTextUnicode");
      static HGLOBAL (WINAPI *GetLineTextUnicode)(int nLineNo)                = (decltype(GetLineTextUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetLineTextUnicode");
      static BOOL (WINAPI *GetCursorPosUnicode)(int* pnLineNo, int* pnColumn) = (decltype(GetCursorPosUnicode))GetProcAddress(hHidemaruExeInstance, "Hidemaru_GetCursorPosUnicode");
      static BOOL (WINAPI *CheckQueueStatus)(void)                            = (decltype(CheckQueueStatus))GetProcAddress(hHidemaruExeInstance, "Hidemaru_CheckQueueStatus");
    
      ・・・
    
      // 現在のカーソルのポジションを得る(1オリジン)
      Point^ GetCursorPos() {
        if (!GetCursorPosUnicode) {
          return gcnew Point(1, 1);
        }
    
        // ネイティブで秀丸エディタに問い合わせ
        int nLineNo = 0;
        int nColumn = 0;
        GetCursorPosUnicode(&nLineNo, &nColumn);
        // マネージドでPoint参照型にしておく
        Point^ p = gcnew Point(nLineNo, nColumn);
        return p;
      }
    
      void LoadCurText() {
        if (!GetTotalTextUnicode) {
          return;
        }
        Point^ p = GetCursorPos();
        OutputDebugStream(L"カーソル位置 x:%d, y:%d\n", p->X, p->Y);
    
        String^ all = "";
    
        // 現在編集中のテキストを得る
        HGLOBAL hGlobal = GetTotalTextUnicode();
        if (hGlobal) {
          // ネイティブでピンポインタ状態で得る
          wchar_t* pwsz = (wchar_t*)GlobalLock(hGlobal);
          // マネージドにコピー
          all = marshal_as<System::String^>(pwsz);
          // ネイティブは解放
          GlobalUnlock(hGlobal);
          GlobalFree(hGlobal);
        }
    
        try {
    
          Regex^ re = gcnew Regex("((^#.+?\n)+)sub\\s+([^\\s]+)", RegexOptions::Multiline); // 1行としてみなして
          MatchCollection^ mc = re->Matches(all);
    
          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^) {
        }
      }
    
    
  • 再度コンパイル

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

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