パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
Scintillaをビルドする

最新のScintillaコンポーネントを自分でビルドしてwxWidgetsライブラリから利用する手順を説明する。

Scintillaはウィンドウズ、リナックスライク(GTKツールキット)、アップルマッキントッシュ(Cocoaフレームワーク)をターゲットとする多機能なエディットコンポーネントである。wxWidgetsにはwxStyledTextCtrlというラッパーコントロールが用意されているが後述の理由で最新版の機能を欠く。内部文字コードがUTF-8デフォルトなのに入出力でwxWidgets文字型(wxString)を経由するのも面白くない。本項目はScintillaを自力でビルドする。これをwxWidgetsで利用するが、Scintillaの機能はラッパーを介さず直接コールする手段を提供して効率化を図る。本項目はバージョン5.3.1を前提に記述する。別に必要となる字句解析ライブラリLexillaはバージョン5.2.0を前提とする

覚え書き
ScintillaはMSYS2でも導入できるのだろうか。pacman -SsはGTK(GNOME)あるいはQtターゲットのみをリストし、これら(あるいはいずれか)がGTK/Qtに依存しない部分のライブラリファイルを供給しているのだろうか。そのうち確認しようと思う。
user@THINKPAD-L430 MSYS ~
$ pacman -Ss scintilla\|scite
...
mingw32/mingw-w64-i686-qscintilla 2.13.3-2
A port to Qt5 of Neil Hodgson's Scintilla C++ editor class (mingw-w64)
mingw32/mingw-w64-i686-scintilla-gtk3 5.1.4-1
A text widget adding syntax highlighting and more to GNOME (mingw-w64)
mingw32/mingw-w64-i686-scite 4.4.6-1
Editor with facilities for building and running programs (mingw-w64)
mingw32/mingw-w64-i686-scite-defaults 4.4.6-1
Default language files for the SciTE editor (mingw-w64)
...

Scintillaのバージョン

Scintillaは3.7.6でC++14を用いるメインブランチ(4.0.0以降)と用いないLongTerm3ブランチ(3.X.X)に分岐して、バージョン履歴は二つ存在する。

wxWidgetsライブラリは現時点でC++14でビルドされる一方で、Scintillaメインブランチは4.0.3でC++17にスイッチした。これを理由にwxStyledTextCtrlはLongTerm3ブランチか4.0.3(確実には3.7.6)より前のScintillaを用いざるを得ない。LongTerm3は3.21.1で更新停止して、つまりwxStyledTextCtrlは最新版のScintillaを用いる事は現時点で不可能となった。

2022年11月時点でwxStyledTextCtrlの用いるScintillaバージョンをwxStyledTextCtrl::GetLibraryVersionInfoスタティックメンバ関数で確認した。MSYS2供給wxWidgetsはバージョン3.0.5でScintillaの3.21を用いる。そのwxStyledTextCtrlは日本語IME入力ウィンドウを必ずスクリーン左上隅に表示するという不具合を持つ。最新のwxWidgetsは3.2.1でScintillaの3.7.2を用いて、なぜか3.0.5のそれより古い。いずれにせよScintillaの最新版に遥かに遅れる。

ソースコード

Scintillaは4.0.3から字句解析(lexer)部分をLexillaと名付ける別ライブラリに分離し、5.0.0からプロジェクト自体を分離した。本項目は両者を共に導入するものの、それぞれが別プロジェクトとしてリリースされ異なるバージョン番号を持つため、ルートディレクトリを共有としながら異なるディレクトリに展開する。

ダウンロード

リンク先からscintilla531.ziplexilla520.zipをダウンロードする。

ディレクトリ配置

ディレクトリは以下とする。異なるバージョンのソースコード展開ディレクトリはScintillaルートの配下に並列させる。

目的 ディレクトリ
Scintillaルート C:\Scintilla
Scintillaソースコードの展開 C:\Scintilla\scintilla531
Scintillaビルドの出力 C:\Scintilla\scintilla531\my_builds\[ビルド名]
Lexillaソースコードの展開 C:\Scintilla\lexilla520
Lexillaビルドの出力 C:\Scintilla\lexilla520\my_builds\[ビルド名]

それぞれのzipファイルをScintillaルートに置きエクスプローラー右クリック[すべて展開]で[圧縮(ZIP)形式フォルダーの展開]ダイアログ[ファイルを以下のフォルダーに展開する]をC:\Scintillaに変更して[展開]する。デフォルトのままではscintilla/lexillaディレクトリをルートとして圧縮されているため一階層余分になる。展開されるディレクトリ名はバージョン番号を含まないscintilla/lexillaなので、これをscintilla531/lexilla520に変更する。本項目での相対パスはC:\Scintilla\scintilla531あるいはC:\Scintilla\lexilla520からとする。

ディレクトリ ファイル 内容
C:\Scintilla
├ scintilla531 README インストール方法の記述
││
│├ bin (デフォルト出力ディレクトリ)
│├ doc (ドキュメントディレクトリ)
│├ include *.h ライブラリインクルード
││ Sintilla.iface インターフェース定義
│├ src *.cxx,*.h ソースコード、インクルード
│├ win32 makefile MinGW用メイクファイル
││ scintilla.mak msvc用メイクファイル
││
│︙
└ lexialla520 README インストール方法の記述
 │
 ├ bin (デフォルト出力ディレクトリ)
 ├ doc (ドキュメントディレクトリ)
 ├ include *.h ライブラリインクルード
 │ LexicalStyles.iface インターフェース定義
 ├ src *.cxx,*.h ソースコード、インクルード
 │ makefile MinGW用メイクファイル
 │ lexilla.mak msvc用メイクファイル
 │
 ︙

ビルド

ビルドはMSYS2ツールで行う。最も簡単にはmingw32/mingw64ターミナルから、Scintillaであればwin32ディレクトリで、Lexillaであればsrcディレクトリで、mingw32-makeを実行すればそれぞれのbinディレクトリにダイナミック/スタティックリンクライブラリファイルを得る。READMEはオプション設定方法を全く記述しないため環境変数による設定方法を推測するが、正確にはそれぞれのメイクファイルを参照いただきたい。

環境変数 内容 説明
DEBUG ビルドターゲット指定 空白でない任意文字列ならデバッグ(-g)、それ以外はリリース(-Os)
DEFINES コンパイルの追加オプション 主にマクロ定義(-D)用
BASE_FLAGS コンパイルの追加オプション -
CXXFLAGS コンパイルとリンクの追加オプション -
RANLIB ranlib(書庫インデックス作成)指定 デフォルトはranlib
WINDRES windres(リソースコンパイラ)指定 Scintillaのみ、デフォルトはwindres

デフォルトで出力先は固定され、オブジェクトファイルはwin32/srcディレクトリ、ライブラリファイルはbinディレクトリに出力される。以下に各ビルドのオプションをまとめるが、従ってデフォルトのままでは共存できない。

ビルド名 サブシステム オプション
debug32 mingw32 -g
release32 -Os -s
debug64 mingw64 -g
release64 -Os -s

ビルド

本サイトは、Scintillaであればwin32ディレクトリに、Lexillaであればsrcディレクトリに、my_builds.batバッチファイルを作成して実行する。my_builds.batはScintillaとLexillaで共通化できるが、Lexillaには一つの問題が残り後述する。my_builds.batはbinディレクトリを用いず、my_buildsディレクトリを追加して各ビルドはその配下に出力して共存する。my_builds.batは最大一つの仮引数を取りmingw32-makeへターゲット名として渡す。my_builds.batはインポートライブラリファイル(mingw-w64はDLLに直接リンクできるので必ずしも必要としない)も作成する。このファイルはメイクファイルの定義外なのでcleanターゲットで消去されないが、my_builds.batは実引数にcleanを与えた場合にmy_buildsディレクトリを消去するので問題とならない。

Lexillaに一つの問題が残る。LexillaはScintillaライブラリインクルードを必要とするが、メイクファイルにパスが../../scintilla/include/Scintilla.hのように直接書き込まれている。本サイトは複数バージョン共存の備えとしてscintillaをscintilla531に変更したため、このままではScintillaライブラリインクルードを発見できない。対処としてC:\Scintilla\scintilla\includeディレクトリを作成してC:\Scintilla\scintilla531\includeをコピーした。これらのファイルがScintilla更新で変更される可能性を否定できない以上、将来に禍根を残した。

my_builds.bat

@rem A batch file to build library files each for Scintilla and Lexilla. The
@rem file builds 32/64 bits and debug/release targets using each supplied
@rem makefile.
@rem Because some MSYS2 tools conflict with windows console commands and cannot
@rem be obtained by the priority by the PATH they must be named by the full
@rem paths. Echo on/off inner do loop is almost impossible (see
@rem https://www.dostips.com/forum/viewtopic.php?t=7257), therefore to
@rem echo the make command line we temporarily create MY_MAKE variable to hold
@rem the line.
@rem Each makefile uses windir environment variable to check if it runs on Win32.
@rem In our case, even it runs on Win32 it uses such MSYS2 tools that the windir
@rem must be cleared. Please note that the variable is not WINDIR, which MSYS2
@rem environment always uses although the documentation to explain it has not
@rem been found for MSYS2 but for Cygwin (see
@rem https://cygwin.com/cygwin-ug-net/using-cygwinenv.html). Unfortunately
@rem Lexilla makefile switches dynamic link file extension using the variable
@rem between .dll (windows) and .so (unix-like). That's the reason why
@rem SHAREDEXTENSION is also set.
@rem To compare parameters such as %1 they must be enclosed in quotations
@rem because they could be empty.
@rem Lexilla makefile hard-codes Scintilla include as ../../scintilla/include
@rem which has no way to be changed. You have to place at least Scintilla
@rem include files there.
@setlocal
@set MKDIR=C:\msys64\usr\bin\mkdir
@set RMDIR=C:\msys64\usr\bin\rm -rf
@pushd %~dp0
@for %%n in (32 64) do @(
setlocal enabledelayedexpansion
set PATH=C:\msys64\mingw%%n\bin;C:\msys64\usr\bin;!PATH!
for %%t in (debug release) do @(
set TARGET=%%t%%n
set MY_DIR_O=../my_builds/!TARGET!/obj
set MY_DIR_BIN=../my_builds/!TARGET!/lib
%MKDIR% -p !MY_DIR_O! !MY_DIR_BIN!
set MY_DEBUG=
if %%t==debug set MY_DEBUG=true
set MY_MAKE=mingw32-make %1 DIR_O=!MY_DIR_O! DIR_BIN=!MY_DIR_BIN! DEBUG=!MY_DEBUG!
echo !TARGET!
echo !MY_MAKE!
!MY_MAKE! windir= LDFLAGS="-shared -static -mwindows -Wl,--out-implib,$@.a" SHAREDEXTENSION=dll
echo.
)
endlocal
)
@if "%1"=="clean" %RMDIR% ../my_builds
@popd

ディレクトリ配置

my_builds.batが出力ディレクトリ(my_builds)に生成するファイルの配置を示す。

ディレクトリ ファイル 内容
C:\Scintilla
├ scintilla531
│├ bin (デフォルト出力先ディレクトリ、使用しない)
│︙
│├ include *.h ライブラリインクルード
│├ src
│├ win32 makefile MinGW用メイクファイル
││ my_builds.bat ビルトバッチ
││
│├ my_builds
││├ debug32
│││├ obj *.o オブジェクト
│││└ lib libscintilla.a スタティックリンクライブラリ
│││ Scintilla.dll ダイナミックリンクライブラリ
│││ Scintilla.dll.a インポートライブラリ
││├ release32
││︙
││├ debug64
│││︙
││└ release64
││ ︙
│︙
├ scintilla
│└ include *.h Lexillaビルド用Scintillaライブラリインクルード
└ lexilla520
 ├ bin (デフォルト出力先ディレクトリ、使用しない)
 ︙
 ├ include *.h ライブラリインクルード
 ├ src makefile MinGW用メイクファイル
 │ my_builds.bat ビルトバッチ
 │
 ├ my_builds
 │├ debug32
 ││├ obj *.o オブジェクト
 ││└ lib liblexilla.a スタティックリンクライブラリ
 ││ liblexilla.dll ダイナミックリンクライブラリ
 ││ liblexilla.dll.a インポートライブラリ
 │├ release32
 ││︙
 │├ debug64
 ││︙
 │└ release64
 │ ︙
 ︙

wxWidgetsライブラリラッパー

最新のScintillaをwxWidgetsライブラリと共に使用するための薄いラッパー(thin wrapper)としてTMyScintillaクラスを作成する。これはwxControlを継承して通常のwxWidgetsコントロールとして振る舞うが、機能は全てScintillaコンポーネントを直接操作する。wxWidgetsのウィンドウズ実装部を利用したため他の実装には使えない。サンプルコードはこのラッパーを利用する。

インターフェース

ウィンドウズ実装のScintillaコンポーネントはSendMessageウィンドウズAPI関数によるメッセージ送出で操作する。TMyScintillaはこれを実行するSendMsgメンバ関数のみを用意する。

メンバ関数 機能 返値 説明 仮引数 説明
TMyScintilla コンストラクタ - - - -
TMyScintilla コンストラクタ - - (Create参照) (Create参照)
Create 2ステップ構築用 bool 構築成功 wxWindow* parent 親コントロール
wxWindowID id ウィンドウID
const wxPoint& pos=wxDefaultPosition 位置
const wxSize& size=wxDefaultSize サイズ
long style=0 (未使用)
const wxValidator& validator=wxDefaultValidator (未使用)
const wxString& name="TMyScintilla" コントロール名
SendMsg メッセージ送出 LRESULT メッセージ処理結果 UINT msg メッセージID
WPARAM wParam 追加のメッセージ固有情報
LPARAM lParam 追加のメッセージ固有情報
GetVersionNumber バージョン番号取得 int Sintillaバージョン番号 - -
(wxControl継承関数) ... ... ... ... ...

イベント

ウィンドウズ実装のScintillaコンポーネントはイベント発生でWM_NOTIFYを送出し、lParamに情報を格納するSCNotification構造体へのポインタを渡す。TMyScintillaはWM_NOTIFYをwxEVT_MY_SCINTILLA_NOTIFYカスタムイベントに置き換えてwxCommandEventを送出する。wxCommandEventインスタンスにはwxCommandEvent::SetClientDataメンバ関数でSCNotification構造体へのポインタを格納する。

カスタムイベント 説明 送出 説明
wxEVT_MY_SCINTILLA_NOTIFY Scintillaイベント wxCommandEvent GetClientDataメンバ関数でSCNotification構造体ポインタを取得できる

ソースコード

wxWidgetsとメッセージループの知見でコーディングを大いに節約する。

  • Scintillaは自らをウィンドウクラスに登録するScintilla::Internal::RegisterClasses関数(win32\ScintillaWin.cxx:3753)を用意する。TMyScintillaはCreateメンバ関数内で登録名"Scintilla"を実引数に与えたwxWindowMSW::MSWCreateControlメンバ関数を呼び出してScintillaコンポーネントのウィンドウハンドル(HWND)を作成し、そのラッパーとしてTMyScintillaを構築する。
  • MSWHandleMessage仮想メンバ関数オーバーライドでWM_GETDLGCODEメッセージをフックする。MSWDefWindowProcメンバ関数は登録時のウィンドウプロシージャからScintillaWin::WndProcメンバ関数(win32\ScintillaWin.cxx:1954)をコールしてWM_GETDLGCODEにDLGC_WANTALLKEYS|DLGC_HASSETSELを返すが、これからDLGC_HASSETSELを除く。wxTE_MULTILINEスタイルを持つwxTextCtrlの処置に倣うもので、除かないとフォーカス取得で全選択する。
  • WM_NOTIFYの捕捉はMSWOnNotify仮想メンバ関数のオーバーライドによる。lParamはNMHDR構造体へのポインタであるがScintillaからの通知(SCN_*)ならばSCNotification構造体に拡張されている。

TMyScintilla.h

#ifndef TMYSCINTILLA_H
#define TMYSCINTILLA_H
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
# include "wx/wx.h"
#endif
wxDECLARE_EVENT(wxEVT_MY_SCINTILLA_NOTIFY,wxCommandEvent);
class TMyScintilla:public wxControl
{
private:
static const char* const nameStr_;
public:
bool MSWHandleMessage
(WXLRESULT* rc,WXUINT nMsg,WXWPARAM wParam,WXLPARAM lParam) override;
bool MSWOnNotify(int idCtrl,WXLPARAM lParam,WXLPARAM* result) override;
public:
TMyScintilla() {}
TMyScintilla(wxWindow *parent,wxWindowID id
,const wxPoint& pos=wxDefaultPosition
,const wxSize& size=wxDefaultSize
,long style=0
,const wxValidator& validator=wxDefaultValidator
,const wxString& name=nameStr_)
{Create(parent,id,pos,size,style,validator,name);}
bool Create(wxWindow *parent,wxWindowID id
,const wxPoint& pos=wxDefaultPosition
,const wxSize& size=wxDefaultSize
,long style=0
,const wxValidator& validator=wxDefaultValidator
,const wxString& name=nameStr_);
LRESULT SendMsg(UINT msg,WPARAM wParam,LPARAM lParam)
{return ::SendMessage(GetHWND(),msg,wParam,lParam);}
static int GetVersionNumber();
};
#endif // TMYSCINTILLA_H

TMyScintilla.cpp

#include "TMyScintilla.h"
#include <ScintillaTypes.h>
#include <Scintilla.h>
#include <ScintillaWin.h>
wxDEFINE_EVENT(wxEVT_MY_SCINTILLA_NOTIFY,wxCommandEvent);
namespace
{
static struct TMyScintillaRegisterClasses
{
TMyScintillaRegisterClasses()
{Scintilla::Internal::RegisterClasses(::GetModuleHandle(nullptr));}
} g_ScintillaRegisterClasses;
}
const char* const TMyScintilla::nameStr_="TMyScintilla";
bool TMyScintilla::MSWHandleMessage
(WXLRESULT* rc,WXUINT nMsg,WXWPARAM wParam,WXLPARAM lParam)
{
if (!wxControl::MSWHandleMessage(rc,nMsg,wParam,lParam))
{*rc=MSWDefWindowProc(nMsg,wParam,lParam);}
if (nMsg==WM_GETDLGCODE) {*rc&=~DLGC_HASSETSEL;}
return true;
}
bool TMyScintilla::MSWOnNotify(int idCtrl,WXLPARAM lParam,WXLPARAM* result)
{
if (auto* nmHdr=reinterpret_cast<NMHDR*>(lParam))
{
auto evt=wxCommandEvent{wxEVT_MY_SCINTILLA_NOTIFY,idCtrl};
evt.SetEventObject(this);
evt.SetClientData(nmHdr);
if (ProcessEvent(evt)) {return true;}
}
return wxControl::MSWOnNotify(idCtrl,lParam,result);
}
bool TMyScintilla::Create(wxWindow *parent,wxWindowID id
,const wxPoint& pos,const wxSize& size,long style
,const wxValidator& validator,const wxString& name)
{
if (!CreateControl(parent,id,pos,size,style,validator,name)) {return false;}
if (!MSWCreateControl(L"Scintilla",style,pos,size,name)) {return false;}
return true;
}
int TMyScintilla::GetVersionNumber()
{
return
#include "../version.txt"
;
}

折り返し表示のバグ

大きなファイルを折り返し表示すると不具合を発生する事があるため、できればそのような運用は避けた方が良い。

イベントの過剰発生

Scintillaコンポーネントは折り返し表示オフがデフォルトで、SCI_SETWRAPMODEメッセージでオンに変更して表示幅を超える文字列を折り返し表示できる。折り返し処理はイベントによるバックグラウンドで行われるが、ウィンドウズ実装で巨大なファイルを読み込んだ場合には不具合を呈する。例えば検証パソコン、Release64ターゲットのプログラムで約20MiBの日本語UTF-8ファイルを読み込んだ場合、バックグラウンド処理の完了におよそ16分を要し、その間不具合が継続する。キー(矢印キーを除く)、マウス、スクロールバー操作は可能だがwxWidgetsアイドルイベントを受け付けず、アプリケーションは終了できない。

これはScintillaアイドルイベントが過剰に生成されるためで、開発者はバグと認識するも長い間放置している。折り返し処理はScintillaアイドルイベントを捉えてバックグラウンドで逐次実行するが、その負荷は当然ながらファイルサイズに依存する。Scintillaアイドルイベントは周期的なタイマーイベントをトリガーとして一定期間に連続して大量生成する。これはイベント処理待ちが空となると一度だけ生成するwxWidgetsアイドルイベントと異なる。この過剰なScintillaアイドルイベントが消費されない限りwxWidgetsアイドルイベントは生成されない。

テキストカーソル移動によるイベント遅延

バックグラウンド折り返し処理中にファイル末尾の付近で矢印キーによるテキストカーソル移動を行うと、処理が完了するまで全てのイベントが遅延される。すなわち全ての操作を受け付けずハング状態となる。これはテキストカーソル移動が残った折り返し処理の全てをフォアグラウンドで実行するためで、その間は他のイベント処理を行わない。該当するソースコードを示す(src\Editor.cxx:855)が、テキストカーソル位置が折り返し処理ペンディングの位置にある場合(currentLine >= wrapPending.start)に全領域を対象に折り返し処理を実行する(WrapLines(WrapScope::wsAll))。

void Editor::MovedCaret(SelectionPosition newPos, SelectionPosition previousPos,
bool ensureVisible, CaretPolicies policies) {
...
if (ensureVisible) {
// In case in need of wrapping to ensure DisplayFromDoc works.
if (currentLine >= wrapPending.start) {
if (WrapLines(WrapScope::wsAll)) {
Redraw();
}
}
...
}
...
}

WrapLines(WrapScope::wsAll)を可視部の処理に限定するWrapLines(WrapScope::wsVisible)に変更する、あるいはそもそも描画ルーチン(src\Editor.cxx:1744)がWrapLines(WrapScope::wsVisible)をコールするのでif (currentLine>=wrapPending.start) {...}を消去してしまう、のいずれかで解決できそうだが、現時点ではバグレポートに留め結論としていない。

Code::Blocksからの利用

Code::Blocksから利用するための設定を示す。パスはDebug32ターゲットを例とするが、他のターゲットもこれに準ずれば良い。

[Linker settings]ページの[Link libraries]リストボックスでScintillaのライブラリファイルを追加する。ダイナミックリンクの場合はScintilla.dllとliblexilla.dllを配布に含む必要がある。スタティックリンクの場合はウィンドウズAPIライブラリを追加する。

リンク ライブラリファイル 追加ウィンドウズAPI
ダイナミック Scintilla.dll.a, liblexilla.dll.a -
スタティック libscintilla.a, liblexilla.a libImm32.a, libmsimg32.a

[Search directories]ページの[Compiler]ページ、[Linker]ページ、[Resource compiler]ページ各リストボックスの探索パスに以下を追加する。[Resource compiler]への追加は不要と思われるが、何も考えずに[Compiler]と同じとした。

ページ 探索パス
[Compiler] C:\Scintilla\scintilla531\include
C:\Scintilla\scintilla531\win32
C:\Scintilla\lexilla520\include
[Linker] C:\Scintilla\scintilla531\my_builds\debug32\lib
C:\Scintilla\lexilla520\my_builds\debug32\lib
[Resource compiler] C:\Scintilla\scintilla531\include
C:\Scintilla\scintilla531\win32
C:\Scintilla\lexilla520\include