CSV抽出プログラムで改良するべきところは、[実行]ボタンを押してファイルを読み書きする箇所で、キャンセルが効くようにすることだ。そのためには、実行中の処理経過を表示する[キャンセル]ボタンを持つダイアログボックスを作らなければならないだろう。
だが、その前に、キャンセルボタンを押した処理をどうやって受けるのか。
現在のプログラムは、図のように流れている。バトンは、[実行]ボタンのOnClickイベントハンドラに渡されており、ファイル入出力が終わるまでは帰ってこない。

図1 現在のプログラムの流れ
これを割り込み可能にするには、バトンは戻して別立てで処理を実行してやればよいのだ。つまりマルチスレッドということだ。ここでは、ファイル入出力の処理用に新しいスレッドを作ればいいのだろう。概念的にはこんな感じだ。

図2 マルチスレッドによるプログラムの流れ
さて、C++Builderでマルチスレッドプログラムを実装するにはどうしたらいいのだろうと調べていたら、「新規作成」ダイアログに、[スレッドオブジェクト]というそのものズバリの項目があった。このメニューを実行したら、次のようなスレッドオブジェクトが作られた。
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// Important: Methods and properties of objects in VCL can only be
// used in a method called using Synchronize, for example:
//
// Synchronize(&UpdateCaption);
//
// where UpdateCaption could look like:
//
// void __fastcall MyThread::UpdateCaption()
// {
// Form1->Caption = "Updated in a thread";
// }
//---------------------------------------------------------------------------
__fastcall MyThread::MyThread(bool CreateSuspended)
: TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
void __fastcall MyThread::Execute()
{
//---- Place thread code here ----
}
//---------------------------------------------------------------------------
このコードを見て分かるように、TThreadというクラスがスレッドを作成して実行してくれるクラスだ。実行内容は、Execute() に記述する。TThreadのメンバーExecute() は、次のように定義されている。
virtual void __fastcall Execute() = 0;
この “= 0” というのは、純粋仮想関数というものだそうだ。自分専用のスレッド(この例ではMyThread)のExecute() では、自分専用の処理を定義する。同じスレッドでも、自分のスレッドだと処理内容が違う。でも、とにかくExecute() で実行されることには変わりはない。

図3 仮想関数
実際どのExecute() が呼び出されるかは、実行時に作られたスレッドの種類によって変わる。しかし、いずれの場合も呼び出し方は決まっているので、プログラミングは可能だ。このような種類の関数には、予約語 virtual を付けて宣言する。だが、実装内容が決まっていない、中身が未定義の場合、呼び出す方法(この場合はExecute() を呼び出す)だけが決まっていることになる。このような関数を純粋仮想関数と読んで、上のように = 0 を記述する。
TThreadのExecute() は、純粋仮想関数なので、中身は未定義だ。だから、必ず継承して、Execute() の中身を定義してやらないと、インスタンスを作ることはできない。
いきなり本番に投入すると混乱するので、TThreadの使い方を確認するために、テストアプリケーションを作ってみようと思う。プログレスバーが増えていく処理に割り込みをかけるようなプログラムで、機能を把握できるだろう。
ユーザーインターフェイスの設計
新規VCLアプリケーションを作成し、フォームにコンポーネントを配置する。レイアウトは次のような感じだ。

図4 ユーザーインターフェイスの設計
このフォームは、MyThreadFormという名前に設定する。フォームを選択して、オブジェクトインスペクタで、NameプロパティをMyThreadFormに設定する。
配置したコンポーネントのプロパティを設定する。
ProgressBar1 |
|
プロパティ |
値 |
Min |
0 |
Max |
100 |
|
|
Button1 |
|
プロパティ |
値 |
Name |
btnStart |
Caption |
Start |
|
|
Button2 |
|
プロパティ |
値 |
Name |
btnSuspend |
Caption |
Suspend |
Enabled |
false |
|
|
Button3 |
|
プロパティ |
値 |
Name |
btnResume |
Caption |
Resume |
Enabled |
false |
|
|
Button4 |
|
プロパティ |
値 |
Name |
btnTerminate |
Caption |
Terminate |
Enabled |
false |
|
|
ここでテストするのは、スレッドの生成/実行のほかに、以下の3つのメソッドだ。それぞれの機能をボタンに割り当てる。
メソッド |
説明 |
Suspend |
スレッドを一時停止する |
Resume |
一時停止したスレッドを再開する |
Terminate |
スレッドの停止要求を発行する |
|
|
Terminateメソッドの説明を、「停止要求を発行する」と回りくどくしたのには理由がある。実は、スレッドを停止させようと、Terminateを呼び出しただけでは止まらない。ボタンのイベントハンドラなどに、こんなコードを書くだけではだめだったのだ。
thread->Terminate();
後で全体像を見せるが、スレッドのExecuteメソッド内で、次のようなチェックコードを書いて、処理ループから抜け出すようにしてやらなくてはならないのだ。
if (Terminated)
break;
Terminateを呼び出したら即終了のほうが分かりやすいが、それだと必要な後始末などができなくてかえって不便なのかもしれない。
スレッドクラスの作成
画面設計が終わったら、スレッドオブジェクトを作成する。[ファイル|新規作成|その他]を選択して、「新規作成」ダイアログを表示したら、[項目カテゴリ]から[C++Builderファイル]を選択する。すると、右側のアイコン一覧に[スレッドオブジェクト]という項目が表示されるので、これを選択して[OK]ボタンをクリックする。

図5 新規作成ダイアログ
表示された「スレッドオブジェクト作成」ダイアログで、次のように入力する。
項目 |
値 |
クラス名 |
MyThread |
名前付きスレッド |
(チェックしない) |
|
|

図6 スレッドオブジェクト作成ダイアログ
作成されたスレッドクラスファイルは、MyThreadClass.cppという名前で保存しておく。[ファイル|名前を付けて保存]を実行する。
次に処理を実装する。実装する場所は、MyThread::Execute() である。
void __fastcall MyThread::Execute()
{
//---- Place thread code here ----
}
ただ、注意しなければならないのは、コメントにもあるように、VCLコンポーネントに対する操作は、Synchronize() メソッド経由で行わなければならない。これは、VCLがメインスレッドで実行されているから、衝突を避けるためだろう。
このテストアプリケーションでは、スレッド内でプログレスバーを更新する。プログレスバーは、TProgressBarというVCLコンポーネントだから、これに対するアクセス(つまり、バーの値を更新するプロパティ設定)は、Synchronize経由でなければならない。
そこで、次のようなコードを記述してみた。
void __fastcall MyThread::UpdateProgressBar()
{
MyThreadForm->ProgressBar1->StepBy(1);
}
//---------------------------------------------------------------------------
void __fastcall MyThread::Execute()
{
for(int i=0; i < 100; i++) {
Synchronize(&UpdateProgressBar);
Sleep(100);
if (Terminated)
break;
}
}
MyThreadFormは、MyThreadForm.hで定義されているので、以下の1行も追加しておく(太字)。
#include <vcl.h>
#pragma hdrstop
#include "MyThreadClass.h"
#include "MyThreadForm.h"
これに伴って、ヘッダファイルも修正する。画面下部の、[MyThreadClass.h]をクリックする。
追加するのは、UpdateProgressBarの定義だ。
class MyThread : public TThread
{
private:
void __fastcall UpdateProgressBar();
protected:
void __fastcall Execute();
public:
__fastcall MyThread(bool CreateSuspended);
};
これでスレッド側の準備は完了だ。あとは、ボタンのイベントハンドラを記述して、スレッドへの操作をテストするだけだ。
スレッド操作のテスト
それぞれのボタンに、スレッド処理を記述する。はじめに、ヘッダファイルの方に、スレッドオブジェクトの定義を追加する。
private: // User declarations
TThread *thread;
btnStartのOnClickイベントには、以下のコードを記述する。
void __fastcall TMyThreadForm::btnStartClick(TObject *Sender)
{
if (thread == NULL)
ProgressBar1->Position = 0;
thread = new MyThread(false);
btnStart->Enabled = false;
btnSuspend->Enabled = true;
btnResume->Enabled = false;
btnTerminate->Enabled = true;
}
}
スレッドを新規に作成し、それぞれのボタンの有効/無効を変更する。スレッドを作成するコード new MyThread() で、引数にfalseを渡しているのは、スレッドを直ちに実行する指定である。もし、先にスレッドを作成しておいて、後で実行したいときは、ここにtrueを指定し、Resume() を呼び出せばよい。
void __fastcall TMyThreadForm:: btnSuspendClick(TObject *Sender)
{
if (thread != NULL) {
thread->Suspend();
btnSuspend->Enabled = false;
btnResume->Enabled = true;
}
}
こちらは、[Resume]ボタンを押したときの処理だ。
void __fastcall TMyThreadForm:: btnResumeClick(TObject *Sender)
{
if (thread != NULL) {
thread->Resume();
btnSuspend->Enabled = true;
btnResume->Enabled = false;
}
}
[Terminate]ボタンを押したときの処理はちょっと異なる。
void __fastcall TMyThreadForm:: btnTerminateClick(TObject *Sender)
{
if (thread != NULL) {
thread->Terminate();
btnStart->Enabled = true;
btnSuspend->Enabled = false;
btnResume->Enabled = false;
btnTerminate->Enabled = false;
}
}
Terminateを呼び出すとスレッドの以下の行でbreakする。
if (Terminated)
break;
さて、ここでスレッドオブジェクトの破棄について考えておかなくてはならない。スレッドオブジェクトを破棄する方法は2つだ。
- スレッドの実行が終了したら自動的に破棄するようにする
- スレッドを呼び出した側で明示的に破棄する
実は、このテストプログラムでは、呼び出した側でスレッドをあれやこれや操作しているが、あまり終了した後のことを考えていなかった。終了してしまったスレッドについては、何も操作したくはないが、それが可能になっている(ボタンが有効になっている)ということは問題だろう。
若干コードを変えてみた(太字部分)。
void __fastcall TMyThreadForm::btnStartClick(TObject *Sender)
{
if (thread == NULL)
ProgressBar1->Position = 0;
thread = new MyThread(true);
thread->OnTerminate = ThreadTerminate;
thread->FreeOnTerminate = true;
thread->Resume();
btnStart->Enabled = false;
btnSuspend->Enabled = true;
btnResume->Enabled = false;
btnTerminate->Enabled = true;
}
}
これは、[Start]ボタンを押したときの処理だ。スレッドを生成して、すぐには実行しないようにして、終了したときに呼び出すメソッドを設定している。
thread->OnTerminate = ThreadTerminate;
このThreadTerminateに相当するのが、以下のメソッドだ。
void __fastcall TMyThreadForm::ThreadTerminate(TObject *Sender)
{
thread = NULL;
btnStart->Enabled = true;
btnSuspend->Enabled = false;
btnResume->Enabled = false;
btnTerminate->Enabled = false;
}
ヘッダファイルにも以下の定義を追加する(太字部分)。
__published: // IDE-managed Components
TProgressBar *ProgressBar1;
TButton *btnStart;
TButton *btnSuspend;
TButton *btnResume;
TButton *btnTerminate;
void __fastcall btnStartClick(TObject *Sender);
void __fastcall btnSuspendClick(TObject *Sender);
void __fastcall btnResumeClick(TObject *Sender);
void __fastcall btnTerminateClick(TObject *Sender);
void __fastcall ThreadTerminate(TObject *Sender);
これで恐らくうまく機能するだろう。

図7 テストプログラムの実行
では、テストを踏まえて、プログラムを改造しよう。以下は、現状のbtnExecuteClickだ。今回切り出してスレッドに処理させるのは、恐らく太字の箇所だ。
void __fastcall TCSVExtract::btnExecuteClick(TObject *Sender)
{
if (lstFields->ItemIndex < 0) {
MessageDlg("フィールドが選択されていません.",
mtError, TMsgDlgButtons() << mbOK, 0);
return;
}
// ファイルを開く
if( SaveDialog1->Execute() ) {
std::ifstream in(OpenDialog1->FileName.c_str());
std::ofstream out(SaveDialog1->FileName.c_str());
if (in.is_open() && out.is_open()) {
char buf[1024]; // 便宜上1行は1024文字未満
// ヘッダ行はそのまま書き出す
in.getline(buf, 1024);
out << buf << std::endl;
// 項目分離用の変数 - 最後に必ず解放
TStringList *items = new TStringList();
// 指定された項目を含む行だけ書き出す
try {
while (!in.eof()) {
in.getline(buf, 1024);
SetCSVText(items, buf);
if (items->Count > lstFields->ItemIndex) {
// 完全一致
if (rdgOption->ItemIndex == 0) {
if(items->Strings[lstFields->ItemIndex] == edtCondition->Text)
out << buf << std::endl;
}
// 部分一致
else {
if(items->Strings[lstFields->ItemIndex].Pos(edtCondition->Text) > 0)
out << buf << std::endl;
}
}
}
} catch (...) {
MessageDlg("ファイル入出力に失敗しました.",
mtError, TMsgDlgButtons() << mbOK, 0);
}
delete items;
}
else {
MessageDlg("ファイルオープンエラー.",
mtError, TMsgDlgButtons() << mbOK, 0);
}
}
}
ここでは、次の2段階の手順で、スレッド化を行おうと思う。
- スレッドオブジェクトを作成して、メインの処理を実装する。
- スレッド実行中に表示される進捗ダイアログを作成する。
ファイル抽出スレッドの作成
では、スレッドオブジェクトを作成しよう。スレッドオブジェクトの名称は、CSVExtractThread とする。

図8 CSVExtractThreadの作成
このファイルは、CSVExtractThread.cppという名称で保存しておく。
まず、前回と同じように、以下の2つのヘッダファイルをインクルードする。
#include <fstream>
#include <string.h>
進捗状況の計算は、読み込んだデータ量の合計÷ファイルサイズで割り出す。そのため、ファイルサイズを取得しておかなければならないので、stat() を使うことにしよう。このために、以下のファイルもインクルードする。
#include <sys/stat.h>
さて、コンストラクタは、必要なすべてのパラメータを引数に与えることにする。スレッド内から、フォーム上のコンポーネントにアクセスするのは、Synchronizeを使わなくてはならないため面倒だし、スレッドオブジェクトとフォームが、相互に依存してしまう。
たとえこの場限りのクラスといえども、相互に依存したようながちがちの設計にしてしまうのは気が引ける。スレッドクラスは、たとえどんなフォームが相手でも、テキストを処理して澄ましていられなければなるまい。
TProgressBarが引数に与えられているのに注意してほしい。このスレッドがVCLに依存してしまうのは悩ましいが、どんなフォーム上のプログレスバーにでも、進捗を表示できるように、このようにした。どんな表示形態でも、それなりに進捗を表示するというようなプログラムが書ければもっと美しいのだが。
__fastcall CSVExtractThread::CSVExtractThread(
AnsiString AInputFile, AnsiString AOutputFile,
AnsiString AConditionText, int AFieldIndex, bool AFullMatching,
TNotifyEvent TerminateMethod, TProgressBar *AProgressBar) : TThread(false)
{
InputFile = AInputFile;
OutputFile = AOutputFile;
ConditionText = AConditionText;
FieldIndex = AFieldIndex;
FullMatching = AFullMatching;
FreeOnTerminate = true;
OnTerminate = TerminateMethod;
ProgressBar = AProgressBar;
}
引数として与えたものは、基本的にすべてメンバー変数に蓄える。これについては、あとでまとめてヘッダファイルに記述しよう。
全体の引越しをする前に、SetCSVTextメソッドも移動しておく。これが悩ましいのは、フォームの方でも、ロード時に使っているからだ。とりあえず、staticメソッドにして、スレッドを生成しなくても使えるようにしておくか。
void __fastcall CSVExtractThread::SetCSVText(TStrings *list, const char *src)
{
char buf[1024]; // 便宜上1行は1024文字未満
list->BeginUpdate();
list->Clear();
strcpy(buf, src);
for (char *ptr = buf, *nptr; ptr && *ptr != '\0'; ptr = nptr) {
nptr = strchr(ptr, ',');
if (nptr) {
*nptr = '\0';
nptr++;
}
list->Add(ptr);
}
list->EndUpdate();
}
次に、ProgressBarの更新である。VCLへのアクセスが発生するため、Synchronizeを使うことになる。あんまり頻繁にアクセスするのは無駄な気がするので、スレッド内でも、バーの値を保持しておいて、その値が変わったときだけProgressBarを更新するようにしてみた。
以下の3つのメソッドを使う。ShowProgressは、Synchronize経由で呼び出す実際のProgressBarの更新だ。ResetProgressは、バーの値を0に設定する最初に呼び出されるべきメソッドになる。UpdateProgressは、そのときに読み込んだデータ量を引数に与えて、進捗状況を計算する。ProgressBarの値を更新する必要があるときには、ShowProgressを呼び出す。
いずれの場合もProgressBarがNULLかどうかを確認していることに注意してほしい。これで、進捗状況を表示しないケースにも対応したつもりだ。
void __fastcall CSVExtractThread::ShowProgress()
{
ProgressBar->Position = Position;
}
//---------------------------------------------------------------------------
void __fastcall CSVExtractThread::ResetProgress()
{
if (ProgressBar != NULL) {
Progress = 0;
Position = 0;
Synchronize(&ShowProgress);
}
}
//---------------------------------------------------------------------------
void __fastcall CSVExtractThread::UpdateProgress(unsigned long delta)
{
if (ProgressBar != NULL) {
Progress += delta;
int pos = (Progress * 100) / InputFileSize;
if (pos != Position) {
Position = pos;
Synchronize(&ShowProgress);
}
}
}
さて、これを使ってスレッドの処理部分を記述する。やっていることはシングルスレッドバージョンと同じだが、今作成した機能などを使って若干記述を変えている(太字部分)。また、コンポーネントの値を参照するのをやめて、メンバー変数を参照している。
void __fastcall CSVExtractThread::Execute()
{
// ファイルサイズを取得しておく
struct stat fstat;
stat(InputFile.c_str(), &fstat);
InputFileSize = fstat.st_size;
ResetProgress();
//
std::ifstream in(InputFile.c_str());
std::ofstream out(OutputFile.c_str());
if (in.is_open() && out.is_open()) {
char buf[1024]; // 便宜上1行は1024文字未満
// ヘッダ行はそのまま書き出す
in.getline(buf, 1024);
// 進捗の書き出し
UpdateProgress(strlen(buf));
//
out << buf << std::endl;
// 項目分離用の変数 - 最後に必ず解放
TStringList *items = new TStringList();
// 指定された項目を含む行だけ書き出す
try {
while (!in.eof()) {
if (Terminated) // スレッドを中断
break;
in.getline(buf, 1024);
// 進捗の書き出し
UpdateProgress(strlen(buf));
//
SetCSVText(items, buf);
if (items->Count > FieldIndex) {
// 完全一致
if (FullMatching) {
if(items->Strings[FieldIndex]
== ConditionText)
out << buf << std::endl;
}
// 部分一致
else {
if(items->Strings[FieldIndex].Pos(
ConditionText) > 0)
out << buf << std::endl;
}
}
}
} catch (...) {
Synchronize(&FileIOError);
}
delete items;
}
else {
Synchronize(&FileOpenError);
}
}
エラーメッセージの出力もメインスレッドの役割なので、以下のように切り出して、Synchronize経由で呼び出す。
void __fastcall CSVExtractThread::FileIOError()
{
MessageDlg("ファイル入出力に失敗しました.",
mtError, TMsgDlgButtons() << mbOK, 0);
}
void __fastcall CSVExtractThread::FileOpenError()
{
MessageDlg("ファイルオープンエラー.",
mtError, TMsgDlgButtons() << mbOK, 0);
}
このクラスに対するヘッダファイルは以下のとおり。
class CSVExtractThread : public TThread
{
private:
AnsiString InputFile, OutputFile, ConditionText;
int FieldIndex;
bool FullMatching;
unsigned long InputFileSize, Progress;
int Position;
TProgressBar *ProgressBar;
void __fastcall FileIOError();
void __fastcall FileOpenError();
void __fastcall ShowProgress();
void __fastcall UpdateProgress(unsigned long delta);
void __fastcall ResetProgress();
protected:
void __fastcall Execute();
public:
__fastcall CSVExtractThread(
AnsiString AInputFile, AnsiString AOutputFile,
AnsiString AConditionText, int AFieldIndex, bool AFullMatching,
TNotifyEvent TerminateMethod, TProgressBar *AProgressBar = NULL);
static void __fastcall SetCSVText(TStrings *list, const char *src);
};
進捗ダイアログの作成
では、続いて進捗状況を表すダイアログを作成しよう。[ファイル|新規作成|その他]メニューで、「新規作成」ダイアログを表示し、[C++Builderプロジェクト|C++Builderファイル]から、[標準ダイアログ(縦並び)]を選択する。

図9 標準ダイアログ(縦並び)
縦か横かはこの際重要ではない。実際使うのは[キャンセル]ボタンだけだからだ。[OK]ボタンをクリックすると、次のようなフォームが表示される。このファイルは、ExtractDialog.cppという名前で保存する。ダイアログの名称も、OKRightDlgとなっているので、この辺から直していこう。
OKRightDlg
プロパティ |
値 |
Name |
ExtractDialog1 |
Caption |
抽出中... |
BorderIcons |
すべての項目をfalseに設定 |
|
|
次に、[OK]ボタンを削除し、ProgressBarを配置して、レイアウトを次のように調整する。

図10 レイアウトの調整
このクラスには、スレッドが終了したときに呼び出されるイベントハンドラメソッドを定義する。
void __fastcall TExtractDialog1::CSVExtractTerminated(TObject *Sender)
{
Close();
}
このメソッドは、ダイアログを実行するExecuteメソッド内で、スレッドを作成するときの引数に使う。作成したCSVExtractThread は、threadというメンバーに格納しておくことにしよう。これは、後でヘッダファイルにも定義する。
void __fastcall TExtractDialog1::Execute(
AnsiString AInputFile, AnsiString AOutputFile,
AnsiString AConditionText, int AFieldIndex, bool AFullMatching)
{
thread = new CSVExtractThread(
AInputFile, AOutputFile, AConditionText,
AFieldIndex, AFullMatching, CSVExtractTerminated, ProgressBar1
);
ShowModal();
}
キャンセルボタンを押したときには、スレッドを終了させる。
void __fastcall TExtractDialog1::CancelBtnClick(TObject *Sender)
{
thread->Terminate();
}
ヘッダファイルには、以下の記述を追加しておく。
#include "CSVExtractThread.h"
class TExtractDialog : public TForm
{
__published:
TButton *CancelBtn;
TBevel *Bevel1;
TProgressBar *ProgressBar1;
void __fastcall CancelBtnClick(TObject *Sender);
void __fastcall CSVExtractTerminated(TObject *Sender);
private:
CSVExtractThread *thread;
public:
virtual __fastcall TExtractDialog(TComponent* AOwner);
void __fastcall Execute(
AnsiString AInputFile, AnsiString AOutputFile,
AnsiString AConditionText, int AFieldIndex, bool AFullMatching);
};
最後に、メインフォームの[実行]ボタンを押したときの処理を書き換える。
void __fastcall TCSVExtract::btnExecuteClick(TObject *Sender)
{
if (lstFields->ItemIndex < 0) {
MessageDlg("フィールドが選択されていません.",
mtError, TMsgDlgButtons() << mbOK, 0);
return;
}
// ファイルを開く
if( SaveDialog1->Execute() ) {
ExtractDialog1->Execute(
OpenDialog1->FileName, SaveDialog1->FileName,
edtCondition->Text, lstFields->ItemIndex,
rdgOption->ItemIndex == 0
);
}
}
以上で完成だ。ちょっと長めのCSVファイルを読み込めば、次のように進捗状況を確認できるはずだ。もちろんキャンセルも可能だ。

図11 進捗状態の表示