第14回 リスト表示への道


参考 第9回 (ドラッグ&ドロップ)、第13回(BBox)


今回はリストビューとディレクトリのファイル一覧取得についてです。
今から作るアプリでは、ディレクトリをドラッグすると、ファイル名の一覧がリスト表示されるものを作ります。
そのために使用するクラスとして、BListView、BStringItem、BDirectoryがあります。


今回のサンプルソースです。

// sample12.cpp

#include <Application.h>        // BApplicationを使うために必要
#include <Window.h>             // BWindowを使うために必要
#include <View.h>               // BViewを使うために必要
#include <Alert.h>			  // BAlertを使うために必要
#include <Button.h>             // BButtonを使うために必要
#include <Box.h>			 // BBoxを使うために必要
#include <StringView.h>		  // BStringViewを使うために必要
#include <Entry.h>			  // entry_refを使うために必要
#include <ListView.h>		  // BListViewを使うために必要
#include <ListItem.h>		  // BStringItemを使うために必要
#include <Directory.h>		  // BDirectoryを使うために必要

// クラスの前方宣言
class MyApp;
class MyWindow;
class MyView;

// メッセージ定数
const uint32 BUTTON_CLICKED = 'bucl';

class MyApp : public BApplication
{
public:
	MyApp();
private:
	MyWindow* m_win;
};

class MyWindow : public BWindow
{
public:
	MyWindow(BRect frame,const char* title,window_type type,uint32 flags);
	virtual bool QuitRequested();	// 終了が要求されたときに呼ばれる関数
private:
	MyView* m_view;
};
 
class MyView : public BView
{
public:
	MyView(BRect frame,const char *name,uint32 resizingMode,uint32 flags);
	~MyView();
	virtual void AttachedToWindow();	// ビューがウィンドウに取り付けられたとき(子供として登録された後)呼ばれる関数
	virtual void MessageReceived(BMessage* message);
private:
	BStringView* m_string_view;  // 左上の文字列
	BBox*	 m_box;                 // 装飾としてのBBox
	BButton*   m_button;		//  ボタン
	BListView* m_list_view;       // ディレクトリの内容一覧をあらわすリストビュー
};

MyApp::MyApp()
      :BApplication("application/x-vnd.big56-MyApp")
{
	m_win = new MyWindow(BRect(100,100,350,380),"リストビュー",B_TITLED_WINDOW,B_ASYNCHRONOUS_CONTROLS);
	m_win->Show();
}

MyWindow::MyWindow(BRect frame,const char* title,window_type type,uint32 flags)
		:BWindow(frame,title,type,flags)
{
	frame.OffsetTo(0,0);  // 与えられたBRectの左上が(0,0)になるようにする

	m_view = new MyView(frame,"theView",B_FOLLOW_ALL_SIDES,B_WILL_DRAW);
	AddChild(m_view);	
}

bool MyWindow::QuitRequested()
{
// アプリケーションを終了する
	be_app->Quit();
	return true;
}

MyView::MyView(BRect frame,const char* title,uint32 resizingMode,uint32 flags)
	  :BView(frame,title,resizingMode,flags)
{
	m_string_view = new BStringView(BRect(10,5,240,25),"theStringView","ディレクトリをドロップしてください");
	AddChild(m_string_view);

	m_box = new BBox(BRect(10,30,240,240));
	m_box->SetLabel("ファイル一覧");		
	m_button = new BButton(BRect(140,250,240,270),"theButton",
						"選択文字列の表示",new BMessage(BUTTON_CLICKED));
	AddChild(m_box);	
	AddChild(m_button);
	
	SetViewColor(200,200,200);
}


MyView::~MyView()
{
}

// ボタンのターゲット変更
void MyView::AttachedToWindow()
{
	m_button->SetTarget(this);
}

// メッセージ処理
void MyView::MessageReceived(BMessage* message)
{
	
	switch(message->what)
	{
	case B_SIMPLE_DATA:
		{
		entry_ref ref;
		message->FindRef("refs",&ref);
		BDirectory dir(&ref);
		
		// 受け取ったファイルがディレクトリで無かったら何もしない
		if (dir.InitCheck() == B_OK)
		{
//			m_list_view->MakeEmpty(); // リストビューの内容を空に
			BListItem* item;		// 2001/03/06修正
			while((item = m_list_view->RemoveItem(static_cast<int32>(0) ) ) != NULL) 
				delete item;

			entry_ref ref2;
			// ディレクトリの中のファイルを順々に得る
			while (dir.GetNextRef(&ref2) == B_OK) 
			{
				// ファイルの名前を追加
				m_list_view->AddItem(new BStringItem(ref2.name));
			}
		}
		break;
		}
	case BUTTON_CLICKED:
		{
		if (m_list_view->IsEmpty() ) break;  // リストビューが空だったら何もしない
		
		int32 index;
		index = m_list_view->CurrentSelection(); // 現在選択されている場所の位置を得る
		if (index < 0) break;			// なにも選択されていなかったら何もしない
		
		BStringItem* selected_item;
		selected_item = dynamic_cast<BStringItem*>(m_list_view->ItemAt(index));

		// ダウンキャストした結果が失敗したら何もしない
		if (selected_item != NULL)
		{
			// 現在選択されているものを表示
			(new BAlert("Test",selected_item->Text(),"OK"))->Go();
		}
		
		break;
		}
	default: 
		BView::MessageReceived(message);
		break;
	}
}

int main(int argc,char** argv)
{
 	MyApp app;
	app.Run();
	return 0;
}

今回のサンプルを実行すると次のような画面がでます。

実行結果初期画面

この後に、例として/boot/home/config/be/のディレクトリをドロップすると次のような画面になります。

ディレクトリドロップ後

上の画面では、「Demos」が選択されていますが、このときに右下のボタンを押すと

ボタンを押したとき

と、ファイル名のアラートボックスが表示されます。


いつものように、コントロールの構築方法からです。
まずは、BListViewの構築方法です。

m_list_view = new BListView(BRect(20,20,210,190),"theListView");
m_box->AddChild(m_list_view);

BListViewのコンストラクタは次のようになっています。

BListView(BRect frame,
	const char *name,
	list_view_type type = B_SINGLE_SELECTION_LIST,
	uint32 resizeMask = B_FOLLOW_LEFT |
				B_FOLLOW_TOP,
	uint32 flags = B_WILL_DRAW | B_FRAME_EVENTS |
				B_NAVIGABLE);

コンストラクタで指定出来るものは

第3引数のtype以外の説明は毎度のごとく、第4回に譲ります。
typeで指定できる引数は次の通りです。

説明
B_SINGLE_SELECTION_LIST一つだけ選択可能
B_MULTIPLE_SELECTION_LIST複数選択可能

今回のアプリケーションでは、デフォルトの値のB_SINGLE_SELECTION_LISTを指定しています。
これにより、リスト内の項目はひとつだけ選択可能になっています。


今回のキモはほとんど、MyView::MessageReceived()関数の中身です。
まずは、ディレクトリやその他のファイルなどがドラッグされたときの動作を見てみましょう。

case B_SIMPLE_DATA:
	{
	entry_ref ref;
	message->FindRef("refs",&ref);
	BDirectory dir(&ref);
	
	
		
	// 受け取ったファイルがディレクトリで無かったら何もしない
	if (dir.InitCheck() == B_OK)
	{
//		m_list_view->MakeEmpty(); // リストビューの内容を空に
		BListItem* item;			 // 2001/03/06修正
		while((item = m_list_view->RemoveItem(static_cast<int32>(0) ) ) != NULL) 
			delete item;

		entry_ref ref2;
		// ディレクトリの中のファイルを順々に得る
		while (dir.GetNextRef(&ref2) == B_OK) 
		{
			// ファイルの名前を追加
			m_list_view->AddItem(new BStringItem(ref2.name));
		}
	}
	break;
	}

まず、B_SIMPLE_DATAを受け取ったら、BMessage::FindRefで、ドロップされたファイルの
entry_ref構造体を取得しています。
これは、第9回でやっている処理と同じです。

次にBDirectoryのコンストラクタにentry_ref構造体を与えて、BDirectoryの構築をしています。
BDirectoryのコンストラクタには次のような種類があります。

BDirectory(const BEntry *entry);
BDirectory(const entry_ref *ref);
BDirectory(const char *path);
BDirectory(const BDirectory *dir, const char *path);
BDirectory(const node_ref *ref);
BDirectory(const BDirectory &dir);

BDirectoryの初期化の方法は、entry_ref構造体以外にも、パス名を直接与えたり、
BDirectoryと相対パスを組み合わせたりとか、BEntryクラス、node_ref構造体を与える方法があります。
今回は単純化のためにentry_ref構造体で初期化しています。

次にInitCheck()関数で適切に初期化されたかどうか調べています。
これは、ドロップされたものがディレクトリでなかったら、何もしないためです。
ディレクトリ以外のentry_refでBDirectoryを初期化したら、InitCheck()関数でB_OK以外の値が返ってきます。
ちなみに、筆者の環境で実験したところ、B_BAD_VALUEが返ってました。
InitCheckの戻り値は次の様になっています。

戻り値
B_OK大丈夫
B_NAME_TOO_LONGパス名(path)が長すぎ
B_ENTRY_NOT_FOUNDディレクとリが存在しない
B_LINK_LIMITシンボリックリンクでの循環ループによるエラー
B_BAD_VALUE無効な値が指定されました
B_NO_MEMORYメモリ不足
B_BUSYノード(ファイル)がビジーでアクセス出来ない
B_FILE_ERRORファイルが無効で処理が出来ない
B_NO_MORE_FDSファイルの開き過ぎでファイル記述子が使用できない

次にBListView::MakeEmpty()関数でリストの内容をいったん空にしています。
修正 2001/03/06
すみません、これはメモリリークでした(汗)。
MakeEmptyはリストの内容を空にはしますが、リストの内容であるBListItemについては、メモリを解放しません。
修正内容については下の方へ書きました。

次に、BDirectory::GetNextRef()関数を用いて、ディレクトリ内のファイルを表すentry_ref構造体を順番に取得しています。
ディレクトリ内のファイルを巡回するには次のようなものがあります。いわゆるGetNextシリーズと呼んでもいいかと思います。

virtual	status_t		GetNextEntry(BEntry *entry, bool traverse = false);
virtual	status_t		GetNextRef(entry_ref *ref);
virtual	int32			GetNextDirents(struct dirent *buf, size_t length,

ファイルの内容を示すものを、BEntryクラスか、entry_ref構造体か、dirent構造体に詰め込んで返す関数です。
dirent構造体については、今回使用しないので説明はパスさせてください(実はよく知らないだけだったりする)。

GetNext~()関数では、一回呼び出すと今どのファイルまでの情報を返したか覚えているわけですが、
途中で、また最初から読み出したい場合、Rewind()関数を呼び出します。

status_t		Rewind();

これの戻り値は、B_OKか、BDirectoryオブジェクトが初期化されていないときにB_NO_INITを返すのみです。

ちなみに、話は少し脱線しますが、ファイルの中身のファイルの個数を返す関数は、BDirectory::CountEntries関数です。

virtual int32 CountEntries();

ファイル名の情報をentry_ref構造体で取得したあとは、リストにどんどん追加していきます。
その時に呼び出している関数が、BListView::AddItem()関数です。

// ファイルの名前を追加
m_list_view->AddItem(new BStringItem(ref2.name));

BListView::AddItem()関数の実際の宣言は次のようになっています。

virtual	bool	AddItem(BListItem *item);
virtual bool	AddItem(BListItem *item, int32 atIndex);

二つのバージョンがありますが、どの場所に挿入するかを、atIndexによって指定するかどうかです。
指定しない場合は、リストの最後に追加されます。

さて、BListView::AddItem()関数の引数はBListItemのポインタなのに、なぜBStringViewへのポインタを渡しているのか?
実は、BStringViewはBListViewからの派生クラスで、リストのアイテムの中でもただの文字列を表すためのクラスなのです。
リストに追加されるアイテムは、BListItemから派生させることによってカスタマイズが可能で、
任意の画像のアイテムをリストに追加したりすることも可能なようになっています。
しかし、BListItemというのは、アイテムを描画する関数、DrawItem関数が純粋仮想関数(中身が存在しない)になっていて、
そのままでは、インスタンスを生成できないようになっています。
このようなクラスを抽象クラスといいます。

BStringItemではこのDrawItem関数で文字列を描画するように、ちゃんと実装されているので、
インスタンスの生成が可能というわけです。

実際にカスタマイズされたアイテムを作りたい場合は、BListItemから派生させた自作のクラスを作り、
DrawItem関数を自分でちゃんと実装すれば、カスタマイズされたアイテムの出来上がりです。

話がちょっと脱線してしまいました。
BStringItemのコンストラクタの説明に移ります。

BStringItem(const char *text, 
	uint32 outlineLevel = 0, bool expanded = true);

コンストラクタで指定出来るものは、

第2、第3引数については、階層構造が表せるBOutlineListViewクラスで使用されるものです。
BListViewクラスでは使用されません。


次に、「選択文字列の表示」ボタンが押された時の処理についてです。
ここは、現在リストビューの中で選択されている場所を、どうやって取得するかのサンプルになっています。

case BUTTON_CLICKED:
	{
	if (m_list_view->IsEmpty() ) break;  // リストビューが空だったら何もしない
		
	int32 index;
	index = m_list_view->CurrentSelection(); // 現在選択されている場所の位置を得る
	if (index < 0) break;			// なにも選択されていなかったら何もしない
	
	BStringItem* selected_item;
	selected_item = dynamic_cast<BStringItem*>(m_list_view->ItemAt(index));

	// ダウンキャストした結果が失敗したら何もしない
	if (selected_item != NULL)
	{
		// 現在選択されているものを表示
		(new BAlert("Test",selected_item->Text(),"OK"))->Go();
	}
	
	break;
	}

まず、BListView::IsEmpty()関数でリストビューが空かどうかを調べています。
戻り値はリストが空だったらtrue、そうで無ければfalseが返ってきます。

次に、BListView::CurrentSelection()関数で、現在選択されている場所の位置の取得を行っています。
宣言は次の様になっています。

int32	CurrentSelection(int32 index = 0) const;

引数のindexは、リストの内容が複数選択可能になっていたときに使用します。
例えば、二つアイテムが選択されていたとしたら、選択されている上の方が0番、下の方が1番となります。
エラーの時は負の値が返ってきます。主に何も選択されていないときに負の値が返ってくるようです。

めでたく、選択されている場所を取得したら、BListView::ItemAt()関数で、実際のリストの内容を取得します。
宣言は次の様になっています。

BListItem *ItemAt(int32 index) const;

ここで、戻り値がBListItemであることに注意してください。
実際にリストの内容になるものは、BListItemの派生クラスであるわけですから、
リストの内容を取得するには、慎重にダウンキャストする必要があります。
そこで、dynamic_castを使用して、BStringItemにダウンキャストして、NULLかどうかのエラーチェックを行っています。
こうしないと、文字列の内容を取得するための、BStringView::Text()関数を呼ぶことが出来ないからです。
今回は、BStringItemしかリストの内容になっていないために、dynamic_castのエラー処理は本来は不要ですが、
エラー処理のサンプルとしてこのようなことを行っています。

めでたくアイテムの内容のBStringItemが取得できたら、Text()関数でリストアイテムの文字列を取得し、
その内容をアラートボックスで表示します。
これで今回のサンプルはめでたく完成です。


リストビューはBListViewとBListItemの絶妙な連携によって形づくられていることがおわかりいただけたでしょうか?
さて、今回のサンプルは少々不完全です。
というのも、ファイルの内容が多くあるディレクトリをドロップした場合に、
リストビューにファイル名のリストが収まりきらないからです。
以下は筆者の環境で、設定ファイルの宝庫である/boot/home/config/settings/をドロップした結果です。

設定ディレクトリのドロップ結果

少しわかりづらいかもしれませんが、ファイル名の頭文字がDのところで終わっています。
これでは、イニシャルDも真っ青ということで、次回はリストビューにスクロールバーをつけることに挑戦したいと思います。


修正 2001/03/06
SHINTAさんの指摘より、リストの内容を空にするコードについてですが、MakeEmpty()関数ではなく、
RemoveItem()関数を使って削除するコードに変更しました。
というのも、リストの内容のBStringItemのメモリの解放を考えてのことです。

BListItem* item;	 // 2001/03/06修正
while((item = m_list_view->RemoveItem(static_cast<int32>(0) ) ) != NULL) 
	delete item;

RemoveItemの宣言です。

virtual BListItem* RemoveItem(int32 index);
virtual bool	RemoveItem(BListItem *item);

RemoveItem()関数では、整数を与えるものと、BListItemへのポインタを与えるものの2つのバージョンがあります。
整数を与えるものでは、その場所のアイテムをリストから削除して、そのリストへのポインタを返します。
アイテムを削除するとリストは詰められます。
引数の場所がリストの範囲外だと、NULLを返します。
0番目はリストの先頭ですので、リストが空の時以外はNULLは返ってきません。
これを利用して、whileループで順々にアイテムを削除して、リストが空になるまでアイテムのメモリを解放(delete)しています。

ちなみに、BListItemへのポインタを与えるものでは、引数に与えたアイテムと同じものが削除されます。
同じアイテム(BStringItemにおける同じ文字列など)が複数あった場合、最初にあったほうのアイテムを削除します。
削除に成功したらtrue、失敗したらfalseを返します。

この場合整数を与えるほうのRemoveItem()関数を使いたいのですが、0をそのまま引数に与えると、
0はC++の大抵の場合、NULLと同義なので、整数を与えるバージョンと、BListItemへのポインタを与えるバージョンのどちらを使用していいのか、
コンパイラが決められないため、エラーとなってしまいます。
そのためにキャストを使用して、整数を与えるバージョンを使用するべきとコンパイラに教えています。
本当は、Effective C++によれば、引数が整数型とポインタ型にオーバーロードするのは、
このようなこと(0を与えたときのあいまいさ)が起こるためによくないんですけどね(第25項 参照)。


[<<前へ] [次へ>>] [戻る]

big56 big56@anet.ne.jp
独自ドメインの取得をするなら 生命保険の切り替えはココ 給料前でお金がない・・
[PR] | ヒーリング会社案内 作成se 転職川口栃木荻窪池袋中国SEO対策消費者金融車 買取テンプレート沖縄旅行免許合宿二輪引越しプレゼントゴルフ会員権留学レーシックマッサージFXアフィリエイトFXホームページ制作デイトレードハワイ旅行タイバンコクハワイ レンタカーベスト ハワイ ホテル レーツバリ島Hawaii hotelsHawaii Activitiesbhhrハワイホテルテキスト広告
【運営会社「パラダイムシフト」サービス】 ハワイ現地オプショナルツアーリラックマ) - ビジネスクラス航空券 - 格安航空券(1) - 格安航空券(2) - 海外ホテル - 韓国旅行 - タイムシェア - ホテル 予約
無料ホームページ - 携帯ホームページ - 無料ホームページ作成 - レンタルサーバー - ブログ - ヴィラ - ハワイ コンドミニアム - バリ島 ホテル - プーケット ホテル - 旅行 口コミ - 旅行情報 - 国際電話 - ホノルルマラソン - 掲示板監視 - 風評被害 - ホテル比較 - ノースウェスト航空 - ファイナルチェッカー