第110章 リストビューのソート


今回は、リストビューのソートについて解説します。 ちょっと面倒ですが、型どおりにやればどうということもありません。

前回のリストビューと同じデータです。(年齢を追加してあります) 「名前」ボタンを押すと名前が昇順に並びました。もう一度押すと 逆の順に並びます。「年齢」ボタンを押すと年齢順に並び変わります。 また、並び替えを行った後その他の表示状態(大きいアイコンなど)を 変更しても並び替えは有効です。



では、どのようにすればよいのでしょうか。 まず簡単な手順を示します。

1.項目データのLV_ITEM構造体のmaskメンバにLVIF_PARAMを加える 2.どのサブアイテムを基準に並び替えるたかを保存する配列を用意 3.通知メッセージでどの列ボタンが押されたかを知る 4.その列の並びが今まで昇順だったら降順に、降順だったら昇順を   表すように2の配列に書き込む 5.ListView_SortItemsマクロを呼び出す   2番目の引数に比較関数の名前を指定します 6.比較関数を書きます(コールバック関数です)   3番目の引数はどのサブアイテムを基準に並び替えるかを表しています   1番目の引数が2番目の引数より先行する場合は負の値を   逆の場合は正の値を返します   同じ場合は0を返します。   

ざっと見るとこんな感じです。一番わかりにくいのが6の アプリケーション定義の比較関数でしょうか。 また、忘れやすいのが1のLVIF_PARAMです。 では、実際のプログラムを見てみましょう。

リソース・スクリプト、アイコンは前章と全く同じです。

// listvw05.cpp #define STRICT #define ID_LISTVIEW 100 #include <windows.h> #include <commctrl.h> #include "resource.h" #define NO_OF_SUBITEM 3 #define UP 1 #define DOWN 2 int sortsubno[NO_OF_SUBITEM]; LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int CALLBACK MyCompProc(LPARAM, LPARAM, LPARAM); BOOL InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); void SetInitialData(HWND); char szClassName[] = "listvw05"; //ウィンドウクラス HINSTANCE hInst; //インスタンスハンドル(保存) HWND hList; //リストビューのハンドル HIMAGELIST hImage, hSmall;//イメージリストハンドル int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }

まず、昇順や降順を表すマークを決めておきます。 簡単のためにUPを1, DOWNを2と定義してみました。0は まだ並び替えが起こっていないことを示します。
どのサブアイテムが並び替えられているかを示す配列を用意します。 (sortsubno) この場合、「名前」「住所」「年齢」とあるのでインデックスは2まで あります。従って配列の大きさを3にしておきます。 「名前」が基準になってソートした場合はsortsubno[0]にUPまたはDOWNを入れます。 「住所」が基準になってソートした場合はsortsubno[1]に、 「年齢」が基準になってソートした場合はsortsubno[2]にそれぞれマークしておきます。

後のところは今までと同じです。

//ウィンドウ・クラスの登録 BOOL InitApp(HINSTANCE hInst) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = "MYMENU"; //メニュー名 wc.lpszClassName = (LPCSTR)szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); return (RegisterClassEx(&wc)); }

これは、毎度同じです。せっかくアイコンを作ったのでそれを指定しても良いですね。

//ウィンドウの生成 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HWND hWnd; hInst = hInstance;//グローバル変数に保存 hWnd = CreateWindow(szClassName, "猫でもわかるリストビュー", //タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 CW_USEDEFAULT, //幅 CW_USEDEFAULT, //高さ NULL, //親ウィンドウのハンドル、親を作るときはNULL NULL, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; }

ここもいつもと同じです。インスタンスハンドルをグローバル変数にコピーしておきました。

//ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id, iSelected; int nItem; LV_DISPINFO *lvinfo; NM_LISTVIEW *pNMLV; char buf[64]; static HWND hEdit; switch (msg) { case WM_COMMAND: switch (LOWORD(wp)) { case IDM_END: SendMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_DEL: while(1) { nItem = ListView_GetNextItem( hList, -1, LVNI_ALL | LVNI_SELECTED); if (nItem == -1) break; ListView_DeleteItem(hList, nItem); ListView_Arrange(hList, LVA_ALIGNLEFT); } break; case IDM_EDIT: iSelected = ListView_GetNextItem(hList, -1, LVNI_ALL | LVNI_SELECTED); ListView_EditLabel(hList, iSelected); break; case IDM_LARGE: SetWindowLong(hList, GWL_STYLE, WS_CHILD | WS_VISIBLE | LVS_EDITLABELS | LVS_ICON); ListView_Arrange(hList, LVA_ALIGNLEFT); break; case IDM_SMALL: SetWindowLong(hList, GWL_STYLE, WS_CHILD | WS_VISIBLE | LVS_EDITLABELS | LVS_SMALLICON); ListView_Arrange(hList, LVA_ALIGNLEFT); break; case IDM_LIST: SetWindowLong(hList, GWL_STYLE, WS_CHILD | WS_VISIBLE | LVS_EDITLABELS | LVS_LIST); break; case IDM_TABLE: SetWindowLong(hList, GWL_STYLE, WS_CHILD | WS_VISIBLE | LVS_EDITLABELS | LVS_REPORT); break; } break; case WM_NOTIFY: if ((int)wp == ID_LISTVIEW) { lvinfo = (LV_DISPINFO *)lp; switch (lvinfo->hdr.code) { case LVN_BEGINLABELEDIT: hEdit = ListView_GetEditControl(hList); break; case LVN_ENDLABELEDIT: GetWindowText(hEdit, buf, sizeof(buf)); ListView_SetItemText(hList, lvinfo->item.iItem, 0, buf); break; case LVN_COLUMNCLICK: pNMLV = (NM_LISTVIEW *)lp; if (sortsubno[pNMLV->iSubItem] == UP) sortsubno[pNMLV->iSubItem] = DOWN; else sortsubno[pNMLV->iSubItem] = UP; ListView_SortItems(hList, MyCompProc, pNMLV->iSubItem); break; } } break; case WM_CREATE: InitCommonControls(); hList = CreateWindowEx(0, WC_LISTVIEW, "", WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_EDITLABELS, 0, 0, 0, 0, hWnd, (HMENU)ID_LISTVIEW, hInst, NULL); SetInitialData(hList); break; case WM_SIZE: MoveWindow(hList, 0, 0, LOWORD(lp), HIWORD(lp), TRUE); ListView_Arrange(hList, LVA_ALIGNLEFT); break; case WM_CLOSE: id = MessageBox(hWnd, "終了してもよいですか", "終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { DestroyWindow(hWnd); } break; case WM_DESTROY: ImageList_Destroy(hImage); ImageList_Destroy(hSmall); PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0; }

前章と殆ど同じです。違うところは通知メッセージのところです。

LVN_COLUMNCLICKメッセージが来たら「列」ボタンが押されたことを 意味します。ヘルプを見ると

LVN_COLUMNCLICK pnmv = (NM_LISTVIEW FAR *) lParam;

となっているのでこの時、lParamを調べるとNM_LISTVIEW構造体の ポインタを取得することができます。

あー、また変な構造体が出てきた!

まー、でも毎回変なのが出てくるので慣れっこになりましたね。

typedef struct tagNM_LISTVIEW { NMHDR hdr; int iItem; int iSubItem; UINT uNewState; UINT uOldState; UINT uChanged; POINT ptAction; LPARAM lParam; } NM_LISTVIEW;

というように定義されています。 今回関係あるのはiSubItemだけです。 これで、どのサブアイテムが押されたかがわかります。

次に前回はそのサブアイテムについて昇順、降順のソートが行われたのか まだソートは行われていないのかを調べます。 これは、グローバル変数のsortsubnoを調べれば良いですね。 前回ソートが行われていなかったり、降順のソートが行われていれば 今回は昇順のソートを行います。前回昇順であれば今回は降順です。 これを書き込んでおきます。

次にListView_SortItemsマクロを呼び出します。

BOOL ListView_SortItems( HWND hwnd, PFNLVCOMPARE pfnCompare, LPARAM lParamSort );

hwndはリストビューのハンドルを指定します。
pfnCompareには、アプリケーション定義の比較関数のポインタを指定します。 比較関数は自分で適当な名前を付けてください。
lParamSortは比較関数に渡されるアプリケーション定義の値を指定します。 この場合サブアイテムのインデックス値となります。比較関数については 後述します。

// アプリケーション定義比較関数 // lp1がlp2より先行する場合は負の値、逆の時は正の値、同じ時は0を返す int CALLBACK MyCompProc(LPARAM lp1, LPARAM lp2, LPARAM lp3) { static LV_FINDINFO lvf; static int nItem1, nItem2; static char buf1[30], buf2[30]; lvf.flags = LVFI_PARAM; lvf.lParam = lp1; nItem1 = ListView_FindItem(hList, -1, &lvf); lvf.lParam = lp2; nItem2 = ListView_FindItem(hList, -1, &lvf); ListView_GetItemText(hList, nItem1, (int)lp3, buf1, sizeof(buf1)); ListView_GetItemText(hList, nItem2, (int)lp3, buf2, sizeof(buf2)); if (sortsubno[(int)lp3] == UP) return(strcmp(buf1, buf2)); else return(strcmp(buf1, buf2) * -1); }

lp1は比較される最初のアイテムに関連した32ビット値です。lp2は比較される アイテムの32ビット値です。つまり、アイテムを2つずつ比較して並べ替えを 行おうということです。何回この比較を行えばよいのかはプログラマはわかりません。 こういうときにコールバック関数が使われます。ウィンドウズが並べ替えが できるだけの資料ができるまで呼び出されます。蛇足ですが数がわからないものを 列挙する時にもコールバック関数が使われます。たとえば今あるウィンドウを 列挙するときなどです。

lp3はどのサブアイテムを基準に並べ替えるかを表しています。

さて、lp1とlp2を比較してlp1が先行する場合はマイナス、lp2が先行するときは プラスの値を返します。lp1とlp2が全く同じ場合は0を返します。

しかし、このままではlp1とlp2のどちらが先行するのかわかりません。 そこで、ListView_FindItemマクロを使ってアイテムのインデックスを取得します。

int ListView_FindItem( HWND hwnd, int iStart, const LV_FINDINFO FAR* plvfi );

hwndはリストビューのハンドルです。
iStartは探し始めるアイテムのインデックスを指定します。指定したインデックスは サーチの対象外です。従って最初から探すときは−1を指定します。
plvfiはLV_FINDINFO構造体へのポインタです。

typedef struct _LV_FINDINFO { UINT flags; LPCTSTR psz; LPARAM lParam; POINT pt; UINT vkDirection; } LV_FINDINFO;

flagsはサーチタイプを指定します。この場合lParamをベースとして サーチするのでLVFI_PARAMを指定します。
pszはflagをLVFI_STRINGかLVFI_PARTIALを指定したとき有効です。
lParamはLVFI_PARAMを指定したときに有効です。(このプログラムの場合lp1やlp2を指定します。)
ptはLVFI_NEARESTXYが指定されているとき有効です。
vkDirectionはLVFI_NEARESTXYが指定されているとき有効です。
さて、アイテムのインデックスがわかったら具体的な内容を取得します。 これはListView_GetItemTextマクロを使います。

void WINAPI ListView_GetItemText( HWND hwnd, int iItem, int iSubItem, LPSTR pszText, int cchTextMax );

hwndはリストビューのハンドルを指定します。
iItemはリストビューアイテムのインデックスを指定します。
iSubItemはサブアイテムのインデックスを指定します。0ならば項目ラベルとなります
pszTextは取得する項目またはサブアイテムのバッファへのポインタを指定します。
cchTextMaxはバッファサイズを指定します。

これで具体的な文字列を取得することができました。 あとは、lp1とlp2ではどちらが先行するかを調べれば良いだけとなりました。 これにはstrcmp関数を使えばよいですね。念のためにプロトタイプを書いておきます。

int strcmp( const char *string1, const char *string2 );

string1とstring2を辞書式に比較します。string1が先行するときは負の値、 string2が先行するときは正の値、全く同じ時は0を返します。

こりゃ、そのまま使えますね。sortsubnoを調べて昇順または0ならこの値を そのまま返します。降順なら−1をかけて返します。

// 初期データ表示用 void SetInitialData(HWND hList) { LV_COLUMN lvcol; LV_ITEM item; hImage = ImageList_Create(32, 32, ILC_COLOR4, 4, 0); hSmall = ImageList_Create(16, 16, ILC_COLOR4, 4, 0); ListView_SetImageList(hList, hImage, LVSIL_NORMAL); ListView_SetImageList(hList, hSmall, LVSIL_SMALL); ImageList_AddIcon(hImage, LoadIcon(hInst, "MYICON1")); ImageList_AddIcon(hImage, LoadIcon(hInst, "MYICON2")); ImageList_AddIcon(hImage, LoadIcon(hInst, "MYICON3")); ImageList_AddIcon(hImage, LoadIcon(hInst, "MYICON4")); ImageList_AddIcon(hSmall, LoadIcon(hInst, "S1")); ImageList_AddIcon(hSmall, LoadIcon(hInst, "S2")); ImageList_AddIcon(hSmall, LoadIcon(hInst, "S3")); ImageList_AddIcon(hSmall, LoadIcon(hInst, "S4")); lvcol.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; lvcol.fmt = LVCFMT_LEFT; lvcol.cx = 100; lvcol.pszText = "名前"; lvcol.iSubItem = 0; ListView_InsertColumn(hList, 0, &lvcol); lvcol.cx = 150; lvcol.pszText = "住所"; lvcol.iSubItem = 1; ListView_InsertColumn(hList, 1, &lvcol); lvcol.cx = 50; lvcol.pszText = "年齢"; lvcol.iSubItem = 2; ListView_InsertColumn(hList, 2, &lvcol); item.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; item.pszText = "粂井康孝"; item.iItem = 0; item.iSubItem = 0; item.iImage = 0; item.lParam = 0; ListView_InsertItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "北海道旭川市"; item.iItem = 0; item.iSubItem = 1; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "35"; item.iSubItem = 2; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; item.pszText = "粂井ひとみ"; item.iItem = 1; item.iSubItem = 0; item.iImage = 1; item.lParam = 1; ListView_InsertItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "東京都千代田区"; item.iItem = 1; item.iSubItem = 1; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "36"; item.iItem = 1; item.iSubItem = 2; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; item.pszText = "粂井志麻"; item.iItem = 2; item.iSubItem = 0; item.iImage = 2; item.lParam = 2; ListView_InsertItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "北海道中富良野町"; item.iItem = 2; item.iSubItem = 1; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "18"; item.iItem = 2; item.iSubItem = 2; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; item.pszText = "粂井櫻都"; item.iItem = 3; item.iSubItem = 0; item.iImage = 3; item.lParam = 3; ListView_InsertItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "北海道深川市"; item.iItem = 3; item.iSubItem = 1; ListView_SetItem(hList, &item); item.mask = LVIF_TEXT; item.pszText = "14"; item.iItem = 3; item.iSubItem = 2; ListView_SetItem(hList, &item); return; }

初期データ表示用です。項目をインサートするときLV_ITEM構造体の maskにLVIF_PARAMを指定するのを忘れないでください。忘れると 非常にわかりにくいバグとなります。

今回はいろいろ出てきて少し面倒でしたね。


[SDK第2部 Index] [総合Index] [Previous Chapter] [Next Chapter]

Update Feb/24/1998 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。