最終更新日 2024-09-25

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

概要

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

秀丸 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ファイルを読み込んで、何か入力補完を出し、単語を選択してみましょう。
以下のように、「本当の意味での編集中」のコメント説明文が表示されたでしょうか。