====== EXLSQLITE ====== 作成日: 2023-08-23 (水) [[https://youtu.be/W6pjtaPZgJk|第3回 自作のDLLの関数を呼び出す]] 動画ではほとんど触れることができなかったので、説明をたくさん入れておきました。 ===== プロジェクトの概要 ===== **⚠️このプロジェクトは実験的な目的で作成しました。 着想もコードも不安定です。 現実のプロジェクトに使用するには危険が伴うので、全力で回避してください。** ==== 実行環境 ==== 以下の環境で動作を確認しました。 * Windows Vista 64-bit SP2 (VirtualBoxで実行) * Excel 2000 🐬 * Mingw-w64 x86 (w64devkit i686を利用) * Microsoft Visual Studio 2010 こちらに合わせるために、このような古い環境を用意する必要は全くありません。 確認ができないため、確かなことは言えませんが、おそらく、最新のバージョンでも動作するものと思われます。 ==== 説明 ==== SQLite3のC APIをExcelから利用できるようにすることを目標とします。 SQLite3は、ソースから簡単にDLLにビルドして、CやC++から利用可能な状態にできます。 または、ビルド済みのDLLとして配布されていて、DLLから関数をロードできる機能が備わった言語であれば、そのまま利用できます。 Excel VBAでは、Declare文で適切に関数の宣言をすることで、DLLの関数を直接利用できます。 Excelが32ビット版ならDLLも32ビット、64ビット版ならDLLも64ビットとしてコンパイルされていなければなりません。 32ビット版のExcelには一つ制限があります。 DLL側の関数の呼び出し規約が''%%__stdcall%%''でなければ、読み込みに失敗します。 Mingw-w64、MSVCのC、C++コンパイラは通常、デフォルでは''%%__stdcall%%''ではなく、''%%__cdecl%%''となっています。 SQLiteのソースは、関数の宣言に''%%__stdcall%%''は付与されていません。 そのため、VBAからダイレクトにSQLiteのDLLを使用することができませんでした。 64ビットの環境では、''%%__stdcall%%''は何も意味を持たないので、32ビット版のExcelでのみの制限だと思われますが、64ビット環境では試していないので、本当にそうなのかは分かってません。 32ビット環境でVBAでSQLiteの機能を利用するために、自作のDLLを作成することにしました。 そのDLLでは、SQLiteの関数に対応する名前の関数に''%%__stdcall%%''を付与して用意します。 それらの関数は、単にSQLiteの関数に引数をそのままに転送します。 この自作のDLLは//EXLSQLITE.dll//という名前にしました。 今になっては、//XLSQLITE.dll//の方が良かったと思ってます。 VBAでは、Declare文でこのEXLSQLITE.dllから関数を読み込むことで、間接的にSQLiteの関数を利用することができました。 自作のDLLは、新たな機能を提供するものではありません。 ただ、VBAと、本当に機能を提供するDLLの中間に立って、調整を行う、いわばアダプターのようなものです。 自作のDLLの関数を利用する、という主題からは少しずれていますが、Declare文についての使用例としては有効と思われます。 DLLを利用するだけでなく、読み込んだ関数をVBAのモジュールにラップしてやることで、利用者側からは(理想的には)DLLを意識しないで機能を利用できる形にしました。 こういうことも可能なのだというサンプルにもなるかと思います。 {{ :youtube:exlsqlite3-001.png |VBAからSQLite3のDLLを利用する}} ===== 準備 ===== [[https://www.sqlite.org/index.html|SQLiteのホームページ]]から、以下から必要なものをダウンロードしておいてください。 * [[https://www.sqlite.org/2023/sqlite-amalgamation-3430000.zip|sqlite-amalgamation-3430000.zip]] * 機械的に単一のファイルに凝縮されたソースとヘッダが機能別にいくつか含まれています。 * この中に含まれるsqlite3.hが必須です。 * [[https://www.sqlite.org/2023/sqlite-dll-win32-x86-3430000.zip|sqlite-dll-win32-x86-3430000.zip]] あるいは [[https://www.sqlite.org/2023/sqlite-dll-win64-x64-3430000.zip|sqlite-dll-win64-x64-3430000.zip]] * 32ビットの場合はx86、64ビットの場合はx64の方をダウンロードしてください。 * ビルド済みのDLLとモジュール定義ファイル(.def)が含まれています。 * 簡単にソースをビルドして生成することができるので必須ではないですが、ひと手間省けます。 * [[https://www.sqlite.org/2023/sqlite-tools-win32-x86-3430000.zip|sqlite-tools-win32-x86-3430000.zip]] * ビルド済みのコマンドラインツールです。 * 必須ではないですが、あると便利です。 * 代わりに別途GUIツールを使用することも可能です。「sqlite3 gui」などで検索すると見つかります。例えば、当方は[[https://github.com/little-brother/sqlite-gui|sqlite-gui]]を使用しました。 Excelが32ビットであるか、64ビットであるかを調べて、Mingw-w64をそれに合わせたバージョンでインストールしてください。 例えば、w64devkitを使用するのであれば、[[https://github.com/skeeto/w64devkit/releases|w64devkitのダウンロードページ]]で32ビットの場合**__i686__**がついているものをダウンロードします。64ビットの場合ついていないものをダウンロードします。 適当な場所に展開して、環境変数PATHを適切にセットする必要があります。 このページでは扱いませんが、Visual Studioの場合、よほど古いバージョンでない限りは、どちらのアーキテクチャにするかを選択してコマンドラインツールの環境がセットアップされたコマンドプロンプトを起動できます。 ===== EXLSQLITE3.dll ===== 今回作成する自作のDLLファイルです。 次のようなレイアウトでプロジェクトディレクトリを作成してください。 . └── exlsqlite3/ ├── sqlite-src/ │ ├── sqlite3.h │ └── ...その他のファイル ├── Makefile ├── EXLSQLITE3.h ├── EXLSQLITE3.cpp ├── test_EXLSQLITE3.cpp ├── sqlite3.dll └── sqlite3.def * exlsqlite3/: プロジェクトのディレクトリです。名前は何でもいいです。場所もどこでもいいです。 * sqlite-src/: [[#準備]]のところで入手した、ソースコードのZIPファイルを展開して、この名前に変更してください。 * sqlite3.dll、sqlite3.def: [[#準備]]のところで入手した、ビルド済みDLLの中に含まれるファイルです。ここに配置してください * [[#Makefile]]: 新規に作成してください。 * [[#EXLSQLITE3.h]]: 新規に作成してください。 * [[#EXLSQLITE3.cpp]]: 新規に作成してください。 * [[#test_EXLSQLITE3.cpp]]: テストコードです。 用意ができたら、コマンドプロンプトを立ち上げて、プロジェクトディレクトリに移動します。 そこでmakeを実行すればビルドできます。 例えば、ユーザー名が__happycat__、プロジェクトディレクトリが__C:\code\exlsqlite3__だとします。 C:\Users\happcat\>cd C:\code\exlsqlite3 C:\code\exlsqlite3\>make g++ -c -std=c++20 -Wall -fPIC -o EXLSQLITE3.o EXLSQLITE3.cpp -Isqlite-src -DEXLSQLITE3_EXPORT dlltool -d sqlite3.def -l libsqlite3.dll.a g++ -shared -o EXLSQLITE3.dll EXLSQLITE3.o libsqlite3.dll.a -L. -Wl,--out-implib,libEXLSQLITE3.dll.a,--add-stdcall-alias -lsqlite3 g++ -std=c++20 -Wall -fPIC -o test_EXLSQLITE3.exe test_EXLSQLITE3.cpp -Isqlite-src -L. -Wl,--out-implib,libEXLSQLITE3.dll.a,--add-stdcall-alias libEXLSQLITE3.dll.a C:\code\exlsqlite3\>dir ドライブ C のボリューム ラベルがありません。 ボリューム シリアル番号は 6C66-D6BE です C:\code\exlsqlite3 のディレクトリ 2023/08/25 21:12 . 2023/08/25 21:12 .. 2023/08/25 21:02 7,354 EXLSQLITE3.cpp 2023/08/25 21:12 38,960 EXLSQLITE3.dll 2023/08/25 19:58 3,811 EXLSQLITE3.h 2023/08/25 21:12 3,363 EXLSQLITE3.o 2023/08/25 21:12 24,442 libEXLSQLITE3.dll.a 2023/08/25 21:12 270,020 libsqlite3.dll.a 2023/08/25 18:12 3,503 Makefile 2023/08/25 21:12 sqlite-src 2023/08/21 02:09 7,448 sqlite3.def 2023/05/16 22:47 1,132,091 sqlite3.dll 2023/08/25 18:24 2,924 test_EXLSQLITE3.cpp 2023/08/25 21:12 1,521,705 test_EXLSQLITE3.exe 11 個のファイル 3,015,621 バイト 3 個のディレクトリ 25,036,808,192 バイトの空き領域 __EXLSQLITE3.dll__が生成されていることを確認してください。 ==== Makefile ==== EXLSQLITE3.dllをビルドするためのMakefileです。 * Minigw-w64でしか使用できません * 32ビット版でしかテストしてありません * 64ビット版で使用できるかどうかはわかりません # モジュール名。 # 生成されるファイルのベースとなる名前です。 MODNAME = EXLSQLITE3 # 生成するDLLのファイル名です。 DLL = ${MODNAME}.dll # 生成するDLLのインポートライブラリのファイル名です。 # C、C++でこのDLLを使用する場合に利用できます。 # MinGWのGCCは、インポートライブラリは必須ではありません。 # リンク時にDLLを入力として与えることで、正しくインポートされます。 IMPLIB = lib${MODNAME}.dll.a # モジュール定義ファイルの名前です。 # __stdcall呼び出しが指定された関数は、名前が funcname@4 のように修飾されます。 # そのままではVBA側で読み込むことができないので funcname=funcname@4 のように、別名を与える必要があります。 # MinGWでは、リンカに --add-stdcall-alias を与えることによって、自動で別名を生成させることができるので、必須ではありません。 # MSVCでは、必要です。 # また、MinGWとMSVCで生成される名前が異なるので、注意が必要です。 DEFFILE = ${MODNAME}.def # 標準以外の追加のインクルードパスを指定しています。 # プロジェクトのディレクトリのsqlite-srcというディレクトリにSQLite3のソースコードが置かれていることを想定しています。 CPPFLAGS = -Isqlite-src # GCCのコンパイラオプションです。 # どれも必須ではありません。 # このプロジェクト内ではC++20の機能は使用していません。 # PICはWindowsでは意味を持ちません。 CXXFLAGS = -std=c++20 -Wall -fPIC # リンカのオプションです。 # カレントディレクトリをライブラリパスに追加しています。 # --out-implib,${IMPLIB}は、インポートライブラリを先に指定したファイル名で生成するように指示しています。 # --add-stdcall-alias は、__stdcallが指定された関数に対して、修飾されていない素の関数の名前を別名として生成させる指示です。 LDFLAGS = -L. -Wl,--out-implib,${IMPLIB},--add-stdcall-alias # このDLLがリンクするライブラリを指定します。 # SQLite3のインポートライブラリとリンクすることで、SQLite3の関数がDLLに含まれているという情報を含めることができます。 # MinGWでは、DLLファイルを入力として与えることで、同じことが達成できます。 # このMakefileでは、そうはせずに、インポートライブラリを使用しています。 LDLIBS = -lsqlite3 # SQLite3のモジュール定義ファイル(sqlite3.def)から生成する、SQLite3のインポートライブラリの名前です。 SQLITE3_IMPLIB = libsqlite3.dll.a # デフォルトのターゲットです。 # DLLファイルと、テストプログラムを生成します。 .PHONY: all all: ${DLL} test_${MODNAME}.exe # DLLファイルを生成するルールです。 ${DLL}: ${MODNAME}.o ${SQLITE3_IMPLIB} ${CXX} -shared -o $@ $^ ${LDFLAGS} ${LDLIBS} # DLLファイルに必要な唯一のオブジェクトファイルです。 # -DEXLSQLITE3_EXPORT によって、DLLのヘッダファイルの __declspec(dllexport) が有効になるようにしています。 ${MODNAME}.o: ${MODNAME}.cpp ${MODNAME}.h ${CXX} -c ${CXXFLAGS} -o $@ $< ${CPPFLAGS} -D${MODNAME}_EXPORT # SQLite3のインポートライブラリを生成するルールです。 # MinGWに含まれるdlltoolによって、モジュール定義ファイル(.def)から生成することができます。 ${SQLITE3_IMPLIB}: dlltool -d sqlite3.def -l ${SQLITE3_IMPLIB} # テストプログラムを生成するルールです。 test_${MODNAME}.exe: test_${MODNAME}.cpp ${DLL} ${CXX} ${CXXFLAGS} -o $@ $< ${CPPFLAGS} ${LDFLAGS} ${IMPLIB} # makeによって生成されたファイルを削除します。 .PHONY: clean clean: ${RM} ${DLL} ${IMPLIB} test_${MODNAME}.exe ${SQLITE3_IMPLIB} *.o ==== EXLSQLITE3.h ==== EXLSQLITE3.dllを生成する側と、ユーザーのためのヘッダです。 今回の目標である、VBAから利用するためだけならば不要なものではあります。 しかし、用意しなかったとしても特別に作業が簡略化できるものでもありません。 一方で、このヘッダがなければCとC++から利用することができなくなり不便です。 そういう理由で、ヘッダも用意しました。 #ifndef EXLSQLITE3_H #define EXLSQLITE3_H // DLLを生成する場合は、このマクロを定義することで、関数をエクスポートするようにします。 // このオプションは、コンパイラオプションで指定するのが望ましいかと思います。 // DLLを利用するプログラムでは、このマクロを定義しないでおくことで、dllimportが使用されます。 #ifdef EXLSQLITE3_EXPORT # define DECLSPEC __declspec(dllexport) #else # define DECLSPEC __declspec(dllimport) #endif // 32ビット版ExcelのVBAは、__stdcallが指定された関数しか利用できないようです。 // そのため、すべてのエクスポートする関数を__stdcallにします。 // このDLLがサンプルとして以外に何か役割を果たすとすれば、__stdcall呼び出しでラップする、これだけです。 #define EXLSQLITE3CALL __stdcall // C++としてコンパイルされる場合、ネームマングリングを抑制します。 #ifdef __cplusplus extern "C" { #endif // SQLite3のAPIと同じ値を定義します。 // 他にもたくさんあるのですが、最小限にしてあります。 #define EXLSQLITE_OK 0 #define EXLSQLITE_ROW 100 #define EXLSQLITE_DONE 101 // 構造体の型の宣言です。 // 本体の定義の公開は不要ですし、そうしないほうがよいです。 // ユーザーは関数を通して、ポインタのみでライブラリの機能にアクセスします。 // データの詳細を隠蔽するための、Cで有用なテクニックです。 // SQLite3の提供するインターフェイスを完全に隠蔽したいので、このDLL独自の型を提供しています。 // CとC++では型チェックが機能するので、意図的にキャストしない限り、不適切なポインタを渡すことができなくなります。 // 残念ながら、VBAではそのような恩恵を得ることはできません。 struct exlsqlite3; struct exlsqlite3_stmt; // このDLLがサポートするsqlite3の関数です。 // SQLite3のすべてをカバーする気は全くなくて、ほんの一部となっています。 // どういう基準で選択したかというと、 // - データベースをオープンする // - テーブルを作成する // - データを放り込む // - SELECTして、順に処理する // この手順の中で必要になったものを順次追加していきました。 DECLSPEC int EXLSQLITE3CALL exlsqlite3_open(char const *filename, exlsqlite3 **db); DECLSPEC int EXLSQLITE3CALL exlsqlite3_exec(exlsqlite3 *db, char const *sql, char **errmsg); DECLSPEC int EXLSQLITE3CALL exlsqlite3_prepare(exlsqlite3 *db, char const *sql, exlsqlite3_stmt **pp_stmt); DECLSPEC int EXLSQLITE3CALL exlsqlite3_bind_int(exlsqlite3_stmt *stmt, int index, int value); DECLSPEC int EXLSQLITE3CALL exlsqlite3_bind_text(exlsqlite3_stmt *stmt, int index, char const *text); DECLSPEC int EXLSQLITE3CALL exlsqlite3_bind_text16(exlsqlite3_stmt *stmt, int index, char const *text); DECLSPEC int EXLSQLITE3CALL exlsqlite3_bind_parameter_index(exlsqlite3_stmt *stmt, char const *name); DECLSPEC int EXLSQLITE3CALL exlsqlite3_step(exlsqlite3_stmt *stmt); DECLSPEC int EXLSQLITE3CALL exlsqlite3_reset(exlsqlite3_stmt *stmt); DECLSPEC int EXLSQLITE3CALL exlsqlite3_column_int(exlsqlite3_stmt *stmt, int col); DECLSPEC unsigned char const * EXLSQLITE3CALL exlsqlite3_column_text(exlsqlite3_stmt *stmt, int col); DECLSPEC void const * EXLSQLITE3CALL exlsqlite3_column_text16(exlsqlite3_stmt *stmt, int col); DECLSPEC int EXLSQLITE3CALL exlsqlite3_column_bytes(exlsqlite3_stmt *stmt, int col); DECLSPEC int EXLSQLITE3CALL exlsqlite3_column_bytes16(exlsqlite3_stmt *stmt, int col); DECLSPEC void EXLSQLITE3CALL exlsqlite3_free(void *p); DECLSPEC int EXLSQLITE3CALL exlsqlite3_close(exlsqlite3 *db); DECLSPEC int EXLSQLITE3CALL exlsqlite3_finalize(exlsqlite3_stmt *stmt); // C++の場合の extern "C" を終了します。 #ifdef __cplusplus } #endif #endif ==== EXLSQLITE3.cpp ==== DLLが提供する関数の本体の定義です。 #include "EXLSQLITE3.h" #include // この関数はエクスポートされません。 // DLLのインターフェイスで使用する見せ掛けのexlsqlite3*を 本当のデータ型であるsqlite3*にキャストします。 // reinterpret_castあるいはCスタイルのキャストを直接利用するのではなく、この関数を通して行います。 // 入力元のポインタ型と、変換後のポインタの型を制限するのが目的です。 // そうすることで、誤った型にキャストしてしまうことをコンパイル時の型チェックで検出できる可能性が僅かにあがります。 static sqlite3 *as_sqlite3(exlsqlite3 *db) { return reinterpret_cast(db); } // この関数はエクスポートされません。 // DLLのインターフェイスで使用する見せ掛けの exlsqlite3_stmt* を 実際のデータ型である sqlite3_stmt* にキャストします。 // 直接reinterpret_castあるいはCスタイルのキャストを利用しなり理由は、exlsqlite3*からsqlite3*の場合と同様です。 // こちらのほうが多くの関数で使用されているので、必要度は高いです。 static sqlite3_stmt *as_sqlite3_stmt(exlsqlite3_stmt *stmt) { return reinterpret_cast(stmt); } // // 以下の関数は、すべてエクスポートされます。 // __declspec(dllexport)は、それ以外がヘッダと同一である限り、省いても大丈夫です。 // MSVCの場合に、__stdcallを省略するとヘッダの宣言と異なっている別関数とみなされるので、一致させるように省略しないほうがいいです。 // // 基本的に、exlsqlite3_xxx は、sqlite3_xxx に転送するだけです。 // それらの詳細については、SQLite3のドキュメントを参照してください。 // int EXLSQLITE3CALL exlsqlite3_open(char const *filename, exlsqlite3 **db) { // データベースをオープンします。 // 引数dbはオープンされたデータベースのアドレスが書き込まれます。 // ポインタを出力とする(つまり、関数内でポインタそのものを書き換える)ので、ポインタのポインタを引数としてとります。 return sqlite3_open(filename, reinterpret_cast(db)); } int EXLSQLITE3CALL exlsqlite3_exec(exlsqlite3 *db, char const *sql, char **errmsg) { // 文字列sqlを実行します。 // 第3引数、第4引数は、コールバック関数にかかわる引数となります。 // VBA側からは指定しようがないので、無視することを意味するnullで固定してあります。 // エラーがあった場合、内部で文字列のバッファが確保されて、そのアドレスがerrmsgに書き込まれます。 // このエラーメッセージのバッファをexlsqlite3_free関数で解放する責任は呼び出し側にあります。 return sqlite3_exec(as_sqlite3(db), sql, nullptr, nullptr, errmsg); } int EXLSQLITE3CALL exlsqlite3_prepare(exlsqlite3 *db, char const *sql, exlsqlite3_stmt **pp_stmt) { // 文字列sqlからプリペアードステートメントを作成します。 // sqlite3にはいくつかのバージョンがあり、ここではv2を使用しています。 // 名前が一致していませんが、それ以外のものは提供しないようにしましたので、意図的なものです。 // pp_stmtに作成されたプリペアードステートメントのアドレスが書き込まれます。 // 第3引数は、sqlの最大長を指定します。負の値を指定するとゼロが現れるまで読み込まれます。 // ここでは常に1度に処理すべきSQL文がゼロ終端文字列で渡されるはずだと仮定して、-1に固定しています。 // 第5引数は、1度で処理されなかった場合の、残りのsqlの位置を指すアドレスが書き込まれます。 // nullptrを指定することで無視できるので、簡単のために取り扱わないことにして、固定してあります。 return sqlite3_prepare_v2(as_sqlite3(db), sql, -1, reinterpret_cast(pp_stmt), nullptr); } int EXLSQLITE3CALL exlsqlite3_bind_int(exlsqlite3_stmt *stmt, int index, int value) { // プリペーアドステートメントのindex番目の位置に整数値をバインドします。 return sqlite3_bind_int(as_sqlite3_stmt(stmt), index, value); } int EXLSQLITE3CALL exlsqlite3_bind_text(exlsqlite3_stmt *stmt, int index, char const *text) { // プリペーアドステートメントのindex番目の位置にテキストをバインドします。 // UTF-8でエンコードされていることが期待されます。 // 第4引数は、負数を指定することで、文字列の長さが最初のヌル文字の位置までのバイト数となります。 // ここでは、そのように振る舞うように-1で固定しています。 // 第5引数は、SQLITE_TRANSIENTを指定することで、内部でテキストのコピーが作成され、そのライフサイクルは内部で管理されます。 // ここでは、そのように振る舞うように固定しています。 return sqlite3_bind_text(as_sqlite3_stmt(stmt), index, text, -1, SQLITE_TRANSIENT); } int EXLSQLITE3CALL exlsqlite3_bind_text16(exlsqlite3_stmt *stmt, int index, char const *text) { // プリペーアドステートメントのindex番目の位置にテキストをバインドします。 // UTF-16でエンコードされていることが期待されます。 // 第4引数と第5引数は、sqlite3_bind_textと同様です。 return sqlite3_bind_text16(as_sqlite3_stmt(stmt), index, text, -1, SQLITE_TRANSIENT); } int EXLSQLITE3CALL exlsqlite3_bind_parameter_index(exlsqlite3_stmt *stmt, char const *name) { // プリペーアドステートメントのプレースホルダーのインデックスを取得します。 return sqlite3_bind_parameter_index(as_sqlite3_stmt(stmt), name); } int EXLSQLITE3CALL exlsqlite3_step(exlsqlite3_stmt *stmt) { // プリペアードステートメントを1行ステップします。 // まだ処理すべき行が残っている場合はSQLITE_ROWが、完了した場合はSQLITE_DONEが戻り値となります。 return sqlite3_step(as_sqlite3_stmt(stmt)); } int EXLSQLITE3CALL exlsqlite3_reset(exlsqlite3_stmt *stmt) { // プリペアードステートメントの状態をリセットします。 // 同じSQL文で、パラメータを更新しながら繰り返しINSERTする場合などに便利です。 return sqlite3_reset(as_sqlite3_stmt(stmt)); } int EXLSQLITE3CALL exlsqlite3_column_int(exlsqlite3_stmt *stmt, int col) { // カラム位置の値を整数として取得します。 return sqlite3_column_int(as_sqlite3_stmt(stmt), col); } unsigned char const * EXLSQLITE3CALL exlsqlite3_column_text(exlsqlite3_stmt *stmt, int col) { // カラム位置の値をUTF-8でエンコードされたテキストとして取得します。 // 戻り値がunsigned char const *であることに注意します。 return sqlite3_column_text(as_sqlite3_stmt(stmt), col); } void const * EXLSQLITE3CALL exlsqlite3_column_text16(exlsqlite3_stmt *stmt, int col) { // カラム位置の値をUTF-16でエンコードされたテキストとして取得します。 // 戻り値がvoid const *であることに注意します。 return sqlite3_column_text16(as_sqlite3_stmt(stmt), col); } int EXLSQLITE3CALL exlsqlite3_column_bytes(exlsqlite3_stmt *stmt, int col) { // カラム位置の値をUTF-8でエンコードされたテキストとしたときのバイト数が戻り値として得られます。 return sqlite3_column_bytes(as_sqlite3_stmt(stmt), col); } int EXLSQLITE3CALL exlsqlite3_column_bytes16(exlsqlite3_stmt *stmt, int col) { // カラム位置の値をUTF-16でエンコードされたテキストとしたときのバイト数が戻り値として得られます。 return sqlite3_column_bytes16(as_sqlite3_stmt(stmt), col); } void EXLSQLITE3CALL exlsqlite3_free(void *p) { // sqlite3_malloc、sqlite3_reallocで確保されたメモリを解放します。 // 今回この関数をエクスポートしたのは、sqlite3_execがエラーメッセージで内部的に確保したバッファを解放するために必要だからです。 sqlite3_free(p); } int EXLSQLITE3CALL exlsqlite3_close(exlsqlite3 *db) { // データベースをクローズします。 return sqlite3_close(as_sqlite3(db)); } int EXLSQLITE3CALL exlsqlite3_finalize(exlsqlite3_stmt *stmt) { // プリペアードステートメントを削除します。 return sqlite3_finalize(as_sqlite3_stmt(stmt)); } ==== test_EXLSQLITE3.cpp ==== EXLSQLITE3.dllの不完全なテストです。 #include "EXLSQLITE3.h" #include #include #include #include // テスト用のエントリに対応する構造です。 struct Monkey { int id; std::string name; int age; }; int main() { // // データベースをオープンします。 // exlsqlite3 *db; if (exlsqlite3_open("test_SQLITE3.db", &db) != EXLSQLITE_OK) { std::cerr << "Error: cannot open foo.db\n"; std::exit(1); } // // テーブルを作成します。 // char *err; char const *create_table_sql = "CREATE TABLE IF NOT EXISTS monkey (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"; if (exlsqlite3_exec(db, create_table_sql, &err) != EXLSQLITE_OK) { std::cerr << err << '\n'; exlsqlite3_free(err); std::exit(1); } // // テーブルにデータを1件放り込みます。 // if (exlsqlite3_exec(db, "INSERT INTO monkey (name, age) VALUES ('Tama', 5)", &err) != EXLSQLITE_OK) { std::cerr << err << '\n'; exlsqlite3_free(err); std::exit(1); } // // プリペアードステートメントを使って、テーブルからエントリを取得します。 // exlsqlite3_stmt *stmt; if (exlsqlite3_prepare(db, "SELECT * FROM monkey WHERE age=:age", &stmt) != EXLSQLITE_OK) { std::cerr << "Error: cannot prepare statement\n"; std::exit(1); } // 年齢によってフィルターします。 int index = exlsqlite3_bind_parameter_index(stmt, ":age"); exlsqlite3_bind_int(stmt, index, 5); // プリペアードステートメントを1行ずつステップします。 // 各行の結果を、後で利用するためにコンテナに放り込んでいきます。 std::vector result; while (exlsqlite3_step(stmt) == EXLSQLITE_ROW) { Monkey m; m.id = exlsqlite3_column_int(stmt, 0); // sqlite3_column_textが返す文字列のポインタは unsigned char const * となっています。 unsigned char const *text = exlsqlite3_column_text(stmt, 1); // Cのstrlen関数は、unsigned char const *に利用できません。 // キャストをして無理に利用するより、テキストのバイト数を取得するSQLite3の関数があるので、それを利用します。 auto length = exlsqlite3_column_bytes(stmt, 1); // unsinged char const *ではstd::stringで利用できないので、1文字ずつcharからunsigned charへの変換を行います。 std::transform(text, text + length, std::back_inserter(m.name), [] (unsigned char c) { return static_cast(c); }); m.age = exlsqlite3_column_int(stmt, 2); result.push_back(m); } // // 結果を出力します。 // for (auto x : result) { std::cout << x.id << '|' << x.name << '|' << x.age << '\n'; } // // クリーンアップ処理です。 // exlsqlite3_finalize(stmt); exlsqlite3_close(db); } ===== exlsqlite3.xls ===== ==== 標準モジュール SQLite ==== EXLSQLITE.dllの関数をVBAのモジュールにまとめて、普通のVBAのルーチンのように使用できるような形で提供します。 SQLite3のAPIが返す文字列のポインタから、VBAのストリングを組み立てる方法は、次のサイトを参考にさせてもらいました。 * [[https://codekabinett.com/rdumps.php?Lang=2&targetDoc=api-pointer-convert-vba-string-ansi-unicode|How to convert a (ANSI or Unicode) string pointer to a VBA String]] Option Explicit ' ' Declare文でEXLSQLITE.dllに含まれる関数の参照を宣言します。 ' ' 使用しているExcelのバージョンがとても古いため、ポインタの型としてより適切なLongPtrが使えないため、代わりにLongを使用しています。 ' 現行のExcelのほとんどがLongPtrを使えると思われますので、常にそちらを使用してください。 ' Long型は32ビットなので、64ビットのプログラムではポインタのサイズに互換性がありません。 ' LongPtrならば、32ビット環境なら32ビット、64ビット環境なら64ビットとして扱われるため、どちらにも対応できます。 ' ' 生の文字列データを処理するために必要となる補助関数をkernel32.dllから参照します。 ' 標準Cライブラリにも同じ機能を提供する関数がありますが、kernel32.dllの方がより確実であることが期待できます。 Private Declare Sub RtlMoveMemory Lib "kernel32" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long) 'メモリをコピーする関数です。 Private Declare Function lstrlenA Lib "kernel32" (ByVal lpString As Long) As Long 'ヌル終端ANSI文字列の長さを返す関数です。引数がStringではないことに注意してください。 ' kernel32.dllには、他にも次のような文字列処理の関数があります。 'Private Declare Function lstrcpyA Lib "kernel32" (ByVal lpString1 As String, ByVal lpString2 As Long) As String 'Private Declare Function lstrlenW Lib "kernel32" (ByVal lpString As Long) As Long 'Private Declare Function lstrcpyW Lib "kernel32" (ByVal lpString1 As String, ByVal lpString2 As Long) As String ' 以下はEXLSQLITE3.hに書かれている関数と対応しています。 Private Declare Function exlsqlite3_open Lib "EXLSQLITE3" (ByVal FileName As String, Db As Long) As Long Private Declare Function exlsqlite3_exec Lib "EXLSQLITE3" (ByVal Db As Long, ByVal Sql As String, ErrMsg As Long) As Long Private Declare Function exlsqlite3_prepare Lib "EXLSQLITE3" (ByVal Db As Long, ByVal Sql As String, Stmt As Long) As Long Private Declare Function exlsqlite3_bind_int Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Index As Long, ByVal Value As Long) As Long Private Declare Function exlsqlite3_bind_text Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Index As Long, ByVal Text As String) As Long Private Declare Function exlsqlite3_bind_text16 Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Index As Long, ByVal Text As String) As Long Private Declare Function exlsqlite3_bind_parameter_index Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Name As String) As Long Private Declare Function exlsqlite3_step Lib "EXLSQLITE3" (ByVal Stmt As Long) As Long Private Declare Function exlsqlite3_reset Lib "EXLSQLITE3" (ByVal Stmt As Long) As Long Private Declare Function exlsqlite3_column_int Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Col As Long) As Long Private Declare Function exlsqlite3_column_text Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Col As Long) As Long Private Declare Function exlsqlite3_column_text16 Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Col As Long) As Long Private Declare Function exlsqlite3_column_bytes Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Col As Long) As Long Private Declare Function exlsqlite3_column_bytes16 Lib "EXLSQLITE3" (ByVal Stmt As Long, ByVal Col As Long) As Long Private Declare Sub exlsqlite3_free Lib "EXLSQLITE3" (ByVal P As Long) Private Declare Function exlsqlite3_close Lib "EXLSQLITE3" (ByVal Db As Long) As Long Private Declare Function exlsqlite3_finalize Lib "EXLSQLITE3" (ByVal Stmt As Long) As Long ' SQLite3の関数が返すステータスを表す定数です。 ' SQLite3が設定している値と一致していなければなりません。 ' この値はsqlite3.hで確認できます。 Public Const OK = 0 Public Const ROW = 100 Public Const DONE = 101 ' データベースをオープンします。 ' Dbにオープンされたデータベースのアドレスが入っています。 ' メモリアドレスであることを意識する必要はありません。 ' ハンドルとしてこのモジュールのルーチンや関数に渡して使用します。 ' 戻り値: エラーがなければOKが返され、エラーがあればそれ以外の値が返されます。 Function OpenDatabase(ByVal FileName As String, Db As Long) As Long OpenDatabase = exlsqlite3_open(FileName, Db) End Function ' SQL文を実行します。 ' エラーがあった場合は、ErrMsgにエラーメッセージが設定されます。 ' 戻り値: エラーがなければOKが返され、エラーがあればそれ以外の値が返されます。 Function Exec(ByVal Db As Long, ByVal Sql As String, ErrMsg As String) As Long Dim ErrMsgAddr As Long Dim ErrMsgLen As Long Dim TempStr() As Byte Exec = exlsqlite3_exec(Db, Sql, ErrMsgAddr) If ErrMsgAddr <> 0 Then ErrMsgLen = lstrlenA(ErrMsgAddr) If ErrMsgLen > 0 Then ReDim TempStr(0 To ErrMsgLen - 1) RtlMoveMemory VarPtr(TempStr(0)), ErrMsgAddr, ErrMsgLen ErrMsg = StrConv(TempStr, vbUnicode) End If exlsqlite3_free ErrMsgAddr End If End Function ' Sqlで指定されたSQL文により、プリペアードステートメントを作成します。 ' Stmtに作成されたプリペアードステートメントのアドレスが入っています。 ' アドレスであることは意識せずに、ハンドルとしてこのモジュールのルーチンや関数に渡して使用します。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function Prepare(ByVal Db As Long, ByVal Sql As String, Stmt As Long) As Long Prepare = exlsqlite3_prepare(Db, Sql, Stmt) End Function ' 指定されたインデックスに整数の値をバインドします。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function BindInt(ByVal Stmt As Long, ByVal Index As Long, ByVal Value As Long) As Long BindInt = exlsqlite3_bind_int(Stmt, Index, Value) End Function ' 指定されたインデックスにテキストをバインドします。 ' このテキストはUTF-8でエンコードされているべきです。 ' VBAエディタでは日本語のテキストは正しく入力することができませんので、BindText16を使用します。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function BindText(ByVal Stmt As Long, ByVal Index As Long, ByVal Text As String) As Long BindText = exlsqlite3_bind_text(Stmt, Index, Text) End Function ' 指定されたインデックスにテキストをバインドします。 ' このテキストはUTF-16でエンコードされているべきです。 ' VBAエディタでも日本語のテキストを扱えます。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function BindText16(ByVal Stmt As Long, ByVal Index As Long, ByVal Text As String) As Long Text = StrConv(Text, vbUnicode) BindText16 = exlsqlite3_bind_text16(Stmt, Index, Text) End Function ' プリペアードステートメントに含まれるパラメータ(プレースホルダー)のインデックスを取得します。 ' 戻り値: インデックス Function BindParameterIndex(ByVal Stmt As Long, ByVal Name As String) As Long BindParameterIndex = exlsqlite3_bind_parameter_index(Stmt, Name) End Function ' プリペアードステートメントを1行処理します。 ' 戻り値: ' まだ処理する行が残っていれば、ROWが返されます。 ' すべての処理を終えた場合は、DONEが返されます。 ' エラーなどでその他の値が返されるれることもあります。 Function Step(ByVal Stmt As Long) As Long Step = exlsqlite3_step(Stmt) End Function ' プリペアードステートメントの状態をリセットします。 ' INSERT文で、繰り返し1件ずつパラメータを変化させなら実行するときなどに便利です。 ' 戻り値: やや複雑なので、詳細はSQLite3のドキュメントを参照してください。 ' 通常はエラーがなければOKを返します。 Function Reset(ByVal Stmt As Long) As Long Reset = exlsqlite3_reset(Stmt) End Function ' 指定されたカラムの値を整数値として取得します。 ' 戻り値: カラム位置の整数値 Function ColumnInt(ByVal Stmt As Long, ByVal Col As Long) As Long ColumnInt = exlsqlite3_column_int(Stmt, Col) End Function ' 指定されたカラムの値をテキストとして取得します。 ' テキストはUTF-8でエンコードされています。 ' VBAエディタでは、正しく日本語を扱うことができないので、CoumnText16を使用します。 ' 戻り値: カラム位置のテキスト Function ColumnText(ByVal Stmt As Long, ByVal Col As Long) As String Dim Ptr As Long Dim TextLen As Long Dim TextBuf() As Byte Ptr = exlsqlite3_column_text(Stmt, Col) TextLen = exlsqlite3_column_bytes(Stmt, Col) If TextLen > 0 Then ReDim TextBuf(0 To TextLen - 1) RtlMoveMemory VarPtr(TextBuf(0)), Ptr, TextLen ColumnText = StrConv(TextBuf, vbUnicode) End If End Function ' 指定されたカラムの値をテキストとして取得します。 ' テキストはUTF-16でエンコードされています。 ' VBAエディタでも、正しく日本語を扱うことができます。 ' 戻り値: カラム位置のテキスト Function ColumnText16(ByVal Stmt As Long, ByVal Col As Long) As String Dim Ptr As Long Dim TextLen As Long Dim TextBuf() As Byte Ptr = exlsqlite3_column_text16(Stmt, Col) TextLen = exlsqlite3_column_bytes16(Stmt, Col) If TextLen > 0 Then ReDim TextBuf(0 To TextLen - 1) RtlMoveMemory VarPtr(TextBuf(0)), Ptr, TextLen ColumnText16 = TextBuf End If End Function ' データベースをクローズします。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function CloseDatabase(ByVal Db As Long) As Long CloseDatabase = exlsqlite3_close(Db) End Function ' プリペアードステートメントを削除します。 ' 戻り値: エラーがなければOKが、エラーがあればそれ以外の値が返されます。 Function Finalize(ByVal Stmt As Long) As Long Finalize = exlsqlite3_finalize(Stmt) End Function ' SQLインジェクションを防ぐために、怪しげな文字を排除します。 Function ReplaceBadSqlChars(Sql As String) As String '============================================================================ ' 重要: 不完全な関数です。絶対に本番環境でこのコードを参考にしないでください。 '============================================================================ Dim S As String S = Replace(Sql, ";", "") S = Replace(S, " ", "") S = Replace(S, "'", "\'") ReplaceBadSqlChars = S End Function ' ' このモジュールの不完全なテストを行うルーチンです。 ' Sub test_SQLiteModule() ' EXLSQLITE.dllを読み込むためには、カレントディレクトリをDLLファイルがある場所にする必要があります。 ' Excel起動時のカレントディレクトリがどこであるかの正確な仕様はわかりません。 ' 確実にDLLをロードするために、強制的にカレントディレクトリを変更します。 ChDir "C:\code\exlsqlite3" 'ご自身の環境に合わせたパスに書き換えてください。 Dim Db As Long Dim Stmt As Long Dim ErrMsg As String Dim Index As Long Dim Monkies As Collection Dim M As Monkey ' ' データベースをオープンします。 ' If OpenDatabase("test_SQLiteModule.db", Db) <> OK Then MsgBox "Cannot open foo.db", vbCritical, "Error" GoTo Cleanup End If ' ' (もし存在していなければ) テーブルを作成します。 ' If Exec(Db, "CREATE TABLE IF NOT EXISTS monkey (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", ErrMsg) <> OK Then MsgBox ErrMsg, vbCritical, "Error" GoTo Cleanup End If ' ' INSERTを行うプリペアードステートメントを作成します。 ' If Prepare(Db, "INSERT INTO monkey (name, age) values (:name, :age)", Stmt) <> OK Then MsgBox "Error: prepare", vbExclamation Exit Sub End If ' ' 値をバインドします。 ' Index = BindParameterIndex(Stmt, ":name") BindText16 Stmt, Index, "たま" '日本語文字のテストです。 Index = BindParameterIndex(Stmt, ":age") BindInt Stmt, Index, 220 If Step(Stmt) <> DONE Then MsgBox "Error: insert", vbExclamation GoTo Cleanup End If ' ' SELECTを行うプリペアードステートメントを作成します。 ' テーブルからこれまで作成されたエントリを取り出します。 ' If Prepare(Db, "SELECT * FROM monkey WHERE name <> :name", Stmt) <> OK Then MsgBox "Cannot prepare", vbCritical, "Error" GoTo Cleanup End If Index = BindParameterIndex(Stmt, ":name") If BindText(Stmt, Index, "!!!non-exising-name!!!") <> OK Then MsgBox "Cannot bind parameter", vbCritical, "Error" GoTo Cleanup End If ' ' エントリに対応するオブジェクトを作成して、コレクションに格納します。 ' Set Monkies = New Collection Do While Step(Stmt) = ROW Set M = New Monkey M.Id = ColumnInt(Stmt, 0) M.Name = ColumnText16(Stmt, 1) M.Age = ColumnInt(Stmt, 2) Monkies.Add M Loop ' ' 結果を表示します ' For Each M In Monkies Debug.Print M.Id, M.Name, M.Age Next ' ' 後片付けをします。 ' Cleanup: Finalize Stmt CloseDatabase Db End Sub ==== クラスモジュール Person ==== 単純な値をまとめただけのクラスです。 Collectionに格納するためにはTypeは使用できないので、クラスモジュールとしています。 ' データメンバをPublicにすることに抵抗がある場合、Get/Letプロパティを使用することもできます。 Public Id As Long Public Name As String Public Age As Long Private Sub Class_Initialize() '意味のないダミー値をデフォルト値としておきます。 Id = -1 Name = "No name" Age = -1 End Sub ==== クラスモジュール Monkey ==== モジュールのテストで使用しています。 単純な値をまとめただけのクラスです。 Collectionに格納するためにはTypeは使用できないので、クラスモジュールとしています。 ' データメンバをPublicにすることに抵抗がある場合、Get/Letプロパティを使用することもできます。 Public Id As Long Public Name As String Public Age As Long Private Sub Class_Initialize() '意味のないダミー値をデフォルト値としておきます。 Id = -1 Name = "No name" Age = -1 End Sub ==== Sheet1 (初期データ) ==== SQLiteモジュールを使用して、SQLite3のデータベースを初期化します。 作成されるテーブルのスキーマ CREATE TABLE friendList ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER ) テーブルのエントリはPersonクラスに対応させてあります。 このシートのデータを入力として、テーブルの初期データを作成します。 * A列: name * B列: age A1から空白行が現れるまで、入力データとして読み込みます。 このシートには次のコントロールを配置しておく必要があります。 * CommandButton: * オブジェクト名: FormatTableButton Option Explicit ' ' テーブルを初期化するルーチンです。 ' ' 引数: ' DbName: ' データベースファイルの名前です。 ' ファイルは存在していてもいなくてもかまいません。 ' ただし、パスに含まれるディレクトリは存在している必要があります。 ' TableName: ' 作成するテーブルの名前です。 ' 既存の場合、削除して作り直されます。 ' PersonList: ' Personクラスオブジェクトのコレクションです。 ' 各要素がテーブルのエントリとしてINSERTされます。 Private Sub FormatTable(DbName As String, TableName As String, PersonList As Collection) Dim Db As Long 'データベースのハンドルです。 Dim Stmt As Long 'プリペアードステートメントのハンドルです。 Dim ErrMsg As String 'Exec関数のエラーを取得するための文字列です。 Dim Sql As String 'SQL文のための文字列です。 Dim P As Person '引数PersonListを反復するために使用する変数です。 ' ' データベースをオープンします。 ' If SQLite.OpenDatabase(DbName, Db) <> SQLite.OK Then MsgBox "Cannot open bar.db", vbCritical, "Error" GoTo Cleanup End If ' ' テーブル名に気休め程度のインジェクション対策をかけます。 ' TableName = SQLite.ReplaceBadSqlChars(TableName) ' ' テーブルが存在していたら、削除します。 ' Sql = "DROP TABLE IF EXISTS " & TableName If SQLite.Exec(Db, Sql, ErrMsg) <> SQLite.OK Then MsgBox ErrMsg, vbCritical, "Error" GoTo Cleanup End If ' ' テーブルを作成します。 ' Sql = "CREATE TABLE " & TableName & " (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" If SQLite.Exec(Db, Sql, ErrMsg) <> SQLite.OK Then MsgBox ErrMsg, vbCritical, "Error" GoTo Cleanup End If ' ' INSERTに備えてトランザクションを開始します。 ' トランザクションを使用するのとしないのでは、処理速度に大きな違いが出ます。 ' 入力データの件数が1000件程度になると、トランザクションなしではかなりの遅延が発生します。 ' トランザクションをありならば、即座に完了します。 ' '''' Begin Transaction If SQLite.Exec(Db, "BEGIN TRANSACTION", ErrMsg) <> SQLite.OK Then MsgBox ErrMsg, vbCritical, "Error" GoTo Cleanup End If ' ' INSERTのためのプリペアードステートメントを作成します。 ' Sql = "INSERT INTO " & TableName & " (name, age) VALUES (:name, :age)" If SQLite.Prepare(Db, Sql, Stmt) <> SQLite.OK Then MsgBox "Cannot prepare SQL", vbCritical, "Error" GoTo Cleanup End If ' ' Personのリストでループして、各要素を1行のエントリとしてINSERTしていきます。 ' For Each P In PersonList ' パラメータに値をバインドします。 Dim Index As Long Index = SQLite.BindParameterIndex(Stmt, ":name") SQLite.BindText16 Stmt, Index, P.Name Index = SQLite.BindParameterIndex(Stmt, ":age") SQLite.BindInt Stmt, Index, P.Age ' INSERTは1行だけの処理なので、1つ処理を進めれば完了しているはずで、ステータスはDONEになっているはずです。 If SQLite.Step(Stmt) <> SQLite.DONE Then MsgBox "Failed to insert item " & P.Name & ", " & P.Age, vbExclamation, "Warning" End If ' プリペアードステートメントを再利用するには、リセットが必要です。 If SQLite.Reset(Stmt) <> SQLite.OK Then MsgBox "Failed to reset prepared statement" GoTo Cleanup End If Next ' ' トランザクションをコミットします ' '''' End Transaction If SQLite.Exec(Db, "END TRANSACTION", ErrMsg) <> SQLite.OK Then MsgBox ErrMsg, vbCritical, "Error" GoTo Cleanup End If ' ' 後片付けです。 ' Cleanup: SQLite.Finalize Stmt SQLite.CloseDatabase Db End Sub ' ' 入力データであるPersonを回収します。 ' ' このシートに書かれているA列を名前、B列を年齢としてPersonクラスのオブジェクトを作成します。 ' 1行につき1つのPersonオブジェクトとなります。 ' 作成されたPersonオブジェクトはコレクションに追加されます。 ' A1セルから開始して、A列が空白なるまで回収されます。 ' Private Function CollectFriends() As Collection Dim CurCell As Range 'セルを操作するための変数です。 Dim MaxRow As Integer 'A列の空白セルが現れる直前の行番号です。 Dim FriendList As New Collection '回収したPersonを格納するコレクションです。そのまま戻り値となります。 Dim P As Person 'Personを組み立てるために使用する変数です。 'A1セルから回収を始めます。 Set CurCell = Range("A1") '空セルが現れる直前の行を取得します。 MaxRow = CurCell.End(xlDown).ROW 'Personを回収します。 Do While CurCell.ROW <= MaxRow Set P = New Person P.Name = CurCell.Value P.Age = CurCell.Offset(0, 1).Value FriendList.Add P Set CurCell = CurCell.Offset(1) Loop '戻り値をセットします。 Set CollectFriends = FriendList End Function ' ' タスクを起動するルーチンです。 ' Private Sub RunFormatTableTask() 'EXLSQLITE.dllを読み込むためには、カレントディレクトリをDLLファイルがある場所にする必要があります。 'Excel起動時のカレントディレクトリがどこであるかの正確な仕様はわかりません。 '確実にDLLをロードするために、強制的にカレントディレクトリを変更します。 ChDir "C:\code\exlsqlite3" 'ご自身の環境に合わせたパスに書き換えてください。 ' このシートから入力データを回収します。 Dim FriendList As Collection Set FriendList = CollectFriends ' 回収したPersonのリストを元に、テーブルを初期化します。 FormatTable "exlsqlite3-xls.db", "friendList", FriendList End Sub ' ' シートに配置されたボタン FormatTableButton がクリックされたときの処理です。 ' ' タスクを起動して、完了したらメッセージを表示します。 ' Private Sub FormatTableButton_Click() RunFormatTableTask MsgBox "完了しました。", vbInformation End Sub ==== Sheet2 (SELECT) ==== Sheet1 (初期データ) で作成されたテーブルから、年齢でフィルタリングして結果をこのシートに書き込みます。 このシートには、次のコントロールを配置しておく必要があります。 * CommandButton: * オブジェクト名: RunSelectButton * TextBox: * オブジェクト名: AgeTextBox Option Explicit ' ' 指定されたデータベースとテーブル名から、年齢で絞り込んだ結果を、Personのリストとして返します。 ' Private Function SelectByAge(DbName As String, TableName As String, Age As Long) As Collection Dim Db As Long 'データベースのハンドルです。 Dim Stmt As Long 'SELECT文を実行するプリペアードステートメントのハンドルです。 Dim ErrMsg As String 'Execのエラーメッセージをセットする文字列です。 Dim Sql As String 'SQL文を組み立てるために使用する文字列です。 Dim Index As Long 'プリペアードステートメントのパラメータのインデックスのための変数です。 Dim SelectedPersons As New Collection '結果を格納するためのリストです。 Dim P As Person '結果のPersonを組み立てるために使用する変数です。 ' ' データベースをオープンします。 ' If SQLite.OpenDatabase(DbName, Db) <> SQLite.OK Then MsgBox "Error: cannot open " & DbName, vbCritical GoTo Cleanup End If ' テーブル名に気休め程度のインジェクション対策を施します。 TableName = SQLite.ReplaceBadSqlChars(TableName) ' ' SELECT文を実行するプリペアードステートメントを作成します。 ' Sql = "SELECT * FROM " & TableName & " WHERE age=:age" If SQLite.Prepare(Db, Sql, Stmt) <> SQLite.OK Then MsgBox "Error: cannot prepare statement", vbCritical GoTo Cleanup End If ' ' 年齢をパラメーターにバインドします。 ' Index = SQLite.BindParameterIndex(Stmt, ":age") SQLite.BindInt Stmt, Index, Age ' ' SELECT文の結果を1行ずつ処理して、Personを組み立ててリストに追加していきます。 ' Do While SQLite.Step(Stmt) = SQLite.ROW Set P = New Person With P .Id = SQLite.ColumnInt(Stmt, 0) .Name = SQLite.ColumnText16(Stmt, 1) .Age = SQLite.ColumnInt(Stmt, 2) End With SelectedPersons.Add P Loop ' ' 後片付けです。 ' Cleanup: SQLite.Finalize Stmt SQLite.CloseDatabase Db ' ' 戻り値をセットします。 ' エラーが発生した場合も空のリストを返すために、この位置でセットしています。 ' Set SelectByAge = SelectedPersons End Function ' ' Personの値をこのシートのA1セルから順に書き込んでいきます。 ' ' A列: ID ' B列: 名前 ' C列: 年齢 ' Private Sub WriteResult(PersonList As Collection) Dim CurCell As Range '現在のセル位置を保持する変数です。 Dim P As Person 'Personのリストを反復するための変数です。 'このシートの古いコンテンツを消去します。 Cells.Clear ' ' このシートに結果を書き込みます。 ' Set CurCell = Range("A1") 'A1セルから書き込み始めます。 For Each P In PersonList With CurCell .Offset(0, 0).Value = P.Id .Offset(0, 1).Value = P.Name .Offset(0, 2).Value = P.Age End With Set CurCell = CurCell.Offset(1) Next End Sub Private Sub RunSelectTask(NeedleAge As Long) 'EXLSQLITE.dllを読み込むためには、カレントディレクトリをDLLファイルがある場所にする必要があります。 'Excel起動時のカレントディレクトリがどこであるかの正確な仕様はわかりません。 '確実にDLLをロードするために、強制的にカレントディレクトリを変更します。 ChDir "C:\code\exlsqlite3" 'ご自身の環境に合わせたパスに書き換えてください。 ' ' データベースから指定された年齢に一致するエントリをSELECTして、Personのリストとして回収します。 ' Dim SelectedPersons As Collection Set SelectedPersons = SelectByAge("exlsqlite3-xls.db", "friendList", NeedleAge) ' ' 結果をこのシートに書き込みます。 ' WriteResult SelectedPersons End Sub ' ' このシートに配置されたボタン RunsetSelectButton がクリックされたときの処理です。 ' Private Sub RunSelectButton_Click() ' ' このシートに配置されたテキストボックス AgeTextBox から値を取得して、タスクを起動します。 ' テキストボックスのテキストが数値ではなかった場合は、何もしないでおきます。 ' If IsNumeric(AgeTextBox.Value) Then RunSelectTask AgeTextBox.Value End If End Sub ==== Sheet1 (初期データ)のためのサンプルデータ ==== * ランダムな名前と、0から100までのランダムな数値が1000件含まれたCSVファイルです。 * {{ :youtube:exlsqlite3-sheet1-sample.csv.zip |}} * ランダムな名前は、[[https://1000randomnames.com/|1000 random names]]で生成されたものを利用させてもらいました。 * ランダムな年齢は、セルの数式に ''=INT(RAND() * 101)'' と入力することで得られます。