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