5.コモンコントロール(1) 〜 リストビュウ(1) 〜

 

※ 修正版をこちらのページにアップしています。将来的にはこのページは削除される可能性があります。

 


1.リストビュウ

さて、今回までにドライブの表示はできましたが、ファイルの表示ができていません。
ファイルの表示を行うために、リストビュウ を使うことにします。

リストビュウは、コモンコントロール のコントロールです。つまり標準ではないコントロールになります。
コモンコントロールが標準コントロールと違うところは、標準では提供されないということです。
名前からも当たり前のようですが、CommCtrl.h をインクルードしなくてはいけません。
また、使用ライブラリに ComCtl32.Lib を追加してください。

※ ライブラリの追加は以下のようにやりましょう。

作り方は、コモンコントロールのときとまったく同じですが、使用する前におまじないが必要になります。
C言語には、この手のおまじないがよく出てきます。
InitCommonControlsEx() で使用するコントロールの種類をします。
※ これを行わなくても正常に動きますが、一応おまじないとしてやっておいたほうが無難です。
ダイアログ等のリソースでコモンコントロールを使う場合にのみ、必要だと思うのですが裏が取れませんでした。

BOOL InitCommonControlsEx(
  LPINITCOMMONCONTROLSEX lpInitCtrls  // 登録対象のクラスの情報
);

この引数には、INITCOMMONCONTROLSEX構造体を指定します。
この構造体の第1メンバーには、INITCOMMONCONTROLSEX のサイズ自身を、第2メンバーには、使用しようとするクラス名を指定します。
今回は、リストビュウを使いますから、ICC_LISTVIEW_CLASSES を指定します。

作成するには、標準コントロールと同じように、CreateWindow() で作成することができます。
システムクラスには、WC_LISTVIEW を指定します。
コンボボックスの時には、"COMBOBOX" を指定しましたね。
WC_LISTVIEW は、文字列定数です。表示させてみるとその答えは、"SysListView32" でした。
これは、"CommCtrl.h" を見るとわかりますね。 "COMBOBOX" に対応する文字列定数はないようです。

また、ウィンドウスタイルに WS_CHILD を指定するのを忘れないでくださいね。
ウインドウプロシージャを以下のように修正します。


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  /* 変数宣言部 */
  HINSTANCE         hInstance;
  HWND              hCtlCombo;
  HWND              hCtlListView;
  char              szCurDir[MAX_PATH + 1];
  char              szDrives[128];
  int               i;
  INITCOMMONCONTROLSEX ICC;


  /* 関数本体部 */
  switch (uMsg)
  {
    case WM_COMMAND:
      switch (LOWORD(wParam))
      {
        case IDM_EXIT:
          SendMessage(hWnd, WM_CLOSE, 0, 0L);
          break
        default:
          return (DefWindowProc(hWnd, uMsg, wParam, lParam));
      }
      break;
    case WM_CREATE:
      hInstance = (HINSTANCE) GetWindowLong(hWnd, GWL_HINSTANCE);

      // コンボボックス作成
      hCtlCombo = CreateWindow("COMBOBOX",
                               NULL,
                               WS_CHILD | WS_VISIBLE | CBS_SORT | CBS_DROPDOWN | CBS_AUTOHSCROLL,
                               10,
                               5,
                               380,
                               100,
                               hWnd,
                               0,
                               hInstance,
                               NULL);
      SendMessage(hCtlCombo, WM_SETFONT, (WPARAM) GetStockObject(DEFAULT_GUI_FONT), MAKELPARAM(true , 0));

      GetCurrentDirectory(MAX_PATH + 1, szCurDir);
      ComboBox_SetText(hCtlCombo, szCurDir);
      ComboBox_AddString(hCtlCombo, szCurDir);

      GetLogicalDriveStrings(sizeof(szDrives), szDrives);

      i = 0;
      while (szDrives[i] != '\0')
      {
        ComboBox_AddString(hCtlCombo, &szDrives[i]);

        // 次のドライブ名へ
        while (szDrives[i] != '\0')
        {
          i++;
        }

        i++;
      }

      // コモンコントロール初期化
      ICC.dwSize = sizeof(INITCOMMONCONTROLSEX);
      ICC.dwICC  = ICC_LISTVIEW_CLASSES;
      InitCommonControlsEx(&ICC);

      // リストビュウ作成
      hCtlListView = CreateWindow(WC_LISTVIEW,
                                  NULL,
                                  WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                  10,
                                  30,
                                  380,
                                  320,
                                  hWnd,
                                  0,
                                  hInstance,
                                  NULL);
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    default:
      return (DefWindowProc(hWnd, uMsg, wParam, lParam));
  }

  return 0L;
}

そうして、実行してみてください。
コンボボックスの下になにやらよくわからない物体が表示されました。

sdk_105-03.png


2.カラム(列)のヘッダーを作る

これは、何でしょうか?どうやらリストビュウの上の部分だけが表示されているようです。
これでは何も表示できません。表示するには表示する項目の数だけカラム(列)のヘッダーを作る必要があります。

カラム(列)のヘッダーを作るには、リストビュウに LVM_INSERTCOLUMN メッセージを送る必要があります。
送出するには、 やっぱり SendMessage() を使います。

LVM_INSERTCOLUMN
  wParam = (WPARAM) (int)                   iCol  // カラムを挿入する位置
  lParam = (LPARAM) (const LV_COLUMN FAR *) pCol  // カラムの情報を格納した LVCOLUMN 構造体のポインタ

なんかややこしいことに、LVCOLUMN 構造体 を第2引数に指定しないといけないようです。
LVCOLUMN 構造体 はどのようになっているかというと、

typedef struct _LVCOLUMN {
    UINT   mask;              // どのメンバが有効かを指定
    int    fmt;               // カラムの配置(左端のカラムは左詰め固定)
    int    cx;                // カラムの幅
    LPTSTR pszText;           // カラムのヘッダーの文字列
    int    cchTextMax;        // pszTextのサイズ
    int    iSubItem;          // カラムに関連付けられるインデックス

#if (_WIN32_IE >= 0x0300)     // IE3.0 (comctl32.dll Version4.70) 以降の機能
    int    iImage;            // イメージリストのインデックス
    int    iOrder;            // カラムを挿入した位置
#endif

#if (_WIN32_WINNT >= 0x0600)  // Windows Vista 以降の機能
    int    cxMin;
    int    cxDefault;
    int    cxIdeal;
#endif
} LVCOLUMN, *LPLVCOLUMN;

いやーなんとも難しそうですが、今回使うのは、mask、fmt、cx、pszText、iSubItem の5つです。
mask には今から使用するメンバのフラグを指定します。
メンバとは、構造体の構成要素です。
ここでいうと、fmt、cx、pszText、iSubItem の4つを指定する必要がありますので、LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM を指定します。

fmt には、カラム(列)のヘッダーの配置を指定することができます。LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_CENTER のいずれかを指定します。
今回は、3つのカラム(列)のヘッダー(「名前」、「サイズ」、「更新日時」)を作成しようと考えています。
このため、それぞれ LVCFMT_LEFT、LVCFMT_RIGHT、LVCFMT_LEFT を指定します。

cx は、それぞれのカラム(列)のヘッダーの長さを指定します。
今回は適当に、それぞれ 150、75、100 を指定します。

pszText は、それぞれのカラム(列)のヘッダーのタイトルを指定します。
もちろん、「名前」、「サイズ」、「更新日時」を指定します。

iSubItem は、カラムに関連付けられるインデックスです。
挿入する順番とは関係なく採番することができますが、余計わかりにくくなるので、0 から順につけていくのが無難だといえます。

ウインドウプロシージャを以下のように修正します。
※ 長くなってきたので、必要部分のみ抜粋します。


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  /* 変数宣言部 */
  HINSTANCE         hInstance;
  HWND              hCtlListView;
  LV_COLUMN         LVColumn[] =
                    {
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_LEFT,  150, "名前",     0, 0},
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_RIGHT,  75, "サイズ",   0, 1},
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_LEFT,  110, "更新日時", 0, 2}
                    };
  int               i;
  INITCOMMONCONTROLSEX ICC;


  /* 関数本体部 */
  switch (uMsg)
  {
    case WM_CREATE:
      hInstance = (HINSTANCE) GetWindowLong(hWnd, GWL_HINSTANCE);

      // コモンコントロール初期化
      ICC.dwSize = sizeof(INITCOMMONCONTROLSEX);
      ICC.dwICC  = ICC_LISTVIEW_CLASSES;
      InitCommonControlsEx(&ICC);

      // リストビュウ作成
      hCtlListView = CreateWindowEx(WS_EX_CLIENTEDGE,
                                    WC_LISTVIEW,
                                    NULL,
                                    WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                    10,
                                    30,
                                    380,
                                    320,
                                    hWnd,
                                    0,
                                    hInstance,
                                    NULL);
      for (i = 0; i < (sizeof(LVColumn) / sizeof(LV_COLUMN)); i++)
      {
        ListView_InsertColumn(hCtlListView, i, &LVColumn[i]);
      }
      break;
  }

  return 0L;
}

リストビュウに外枠をつけるために ウィンドウの作成方法を CreateWindow() から CreateWindowEx() に変更しています。
第1引数の拡張ウインドウスタイルには、WS_EX_CLIENTEDGE を指定します。

また、LVM_INSERTCOLUMN を送出するには、SendMessage()を使って、SendMessage(hCtlListView,  LVM_INSERTCOLUMN, 0, (LPARAM) &LVColumn[i]) と書きますが、今回もマクロ ListView_InsertColumn を使用して記述しています。

構造体配列の変数 LVColumn[ ] は宣言時に、初期化を行っています。
ですが、メンバーの数は足りません。
このように、構造体の要素よりも初期値が少ない場合は、仕様として許されていますが、初期値が指定されていない残りのメンバーは 0 に初期化されます。

また、構造体の各メンバーの後についているカンマですが、最後のメンバーに付けても付けなくてもエラーにはなりません。
同様に配列の最後の要素のカンマも付けても付けなくてもいいようです。

なんとなく、それらしい感じになってきていませんか?

sdk_105-04.png


3.配列全体のバイト数?

最後に、解説しておきたいところがあります


  for (i = 0; i < (sizeof(LVColumn) / sizeof(LV_COLUMN)); i++)
  {
    ListView_InsertColumn(hCtlListView, i, &LVColumn[i]);
  }

これは、Loop 文でカラムを追加しているのですが、その回数の指定方法がちょっと特殊です。
今回追加したいカラムは、3 個("名前"、"サイズ"、"更新日時")ですので、


  for for (i = 0; i < 3; i++)
  {
    ListView_InsertColumn(hCtlListView, i, &LVColumn[i]);
  }

と書いてもまったく問題ないわけですが、プログラムの世界では、この 3 をプログラムのコードに直接書くことを嫌う傾向があります。
なぜかというと、今は、3 個ですが、将来カラムの数を増やしたときに 3 を直すのを忘れてバグを誘発することになるからです。
※こういった、定数直書きをマジックナンバーといったりします。
このため、将来カラムの個数を増やしたときも安全なコードを書いておいたのが、先ほどのコードなのです。

何をやっているかと言うと、まず sizeof を使って、LV_COLUMN の配列全体のバイト数を取得します。
それとは別に、sizeof を使って、LV_COLUMN の型自体のバイト数を取得します。
LV_COLUMN の配列全体のバイト数 を LV_COLUMN の型自体のバイト数 で割るということで、3 を導き出しています。
いつものごとく、実際に表示させて見ましょう。

構造体のメンバーは、増えることがよくありますし、OS のバージョンなどによっても変わります。
さらに、構造体のメンバーの型のバイト数を単純に足したものとも異なることがあります。
これは、OS が勝手に扱いやすい形に整形(アラインメント)したりするからです。
int や char の型のバイト数は、わざわざ sizeof しなくてもわかりますので、通常 sizeof は、配列の全体のバイト数に使われますが、構造体の場合は、その型が示すバイト数の取得にも、sizeof が活躍します。
※ 前回 sizeof の例外にちらと触れましたが、早速出てきましたね。

最後に今回の「WinMain.cpp」の全文を載せておきます。


/* ========================================================================== */
/*          FILE        WinMain.cpp                                           */
/*          CREATE      2006/10/01                                            */
/*          MODIFY      2006/12/21                                            */
/*          REMARK                                                            */
/*          COPYRIGHT   2000-2007, Latin. All Rights Reserved                 */
/* ========================================================================== */
#include <Windows.h>
#include <WindowsX.h>
#include <CommCtrl.h>
#include "Resource.h"


/* -------------------------------------------------------------------------- */
/*          CALLBACK    WndProc                                               */
/*          PARAM       HWND        hWnd        : ウィンドウハンドル          */
/*                      UINT        uMsg        : メッセージ                  */
/*                      WPARAM      wParam      : メッセージの追加情報        */
/*                      LPARAM      lParam      : メッセージの追加情報        */
/*          RETURN      LRESULT                                               */
/*                      メッセージ処理の結果                                  */
/*          REMARK                                                            */
/* -------------------------------------------------------------------------- */
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  /* 変数宣言部 */
  HINSTANCE         hInstance;
  HWND              hCtlCombo;
  HWND              hCtlListView;
  LV_COLUMN         LVColumn[] =
                    {
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_LEFT,  150, "名前",     0, 0},
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_RIGHT,  75, "サイズ",   0, 1},
                      {LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM, LVCFMT_LEFT,  110, "更新日時", 0, 2}
                    };
  char              szCurDir[MAX_PATH + 1];
  char              szDrives[128];
  int               i;
  INITCOMMONCONTROLSEX ICC;


  /* 関数本体部 */
  switch (uMsg)
  {
    case WM_COMMAND:
      switch (LOWORD(wParam))
      {
        case IDM_EXIT:
          SendMessage(hWnd, WM_CLOSE, 0, 0L);
          break;
        default:
          return (DefWindowProc(hWnd, uMsg, wParam, lParam));
      }
      break;
    case WM_CREATE:
      hInstance = (HINSTANCE) GetWindowLong(hWnd, GWL_HINSTANCE);
      hCtlCombo = CreateWindow("COMBOBOX",
                               NULL,
                               WS_CHILD | WS_VISIBLE | CBS_SORT | CBS_DROPDOWN | CBS_AUTOHSCROLL,
                               10,
                               5,
                               450,
                               100,
                               hWnd,
                               0,
                               hInstance,
                               NULL);
      SendMessage(hCtlCombo, WM_SETFONT, (WPARAM) GetStockObject(DEFAULT_GUI_FONT), MAKELPARAM(true , 0));

      GetCurrentDirectory(MAX_PATH + 1, szCurDir);
      ComboBox_SetText(hCtlCombo, szCurDir);
      ComboBox_AddString(hCtlCombo, szCurDir);

      GetLogicalDriveStrings(sizeof(szDrives), szDrives);

      i = 0;
      while (szDrives[i] != '\0')
      {
        ComboBox_AddString(hCtlCombo, &szDrives[i]);

        // 次のドライブ名へ
        while (szDrives[i] != '\0')
        {
          i++;
        }

        i++;
      }

      // コモンコントロール初期化
      ICC.dwSize = sizeof(INITCOMMONCONTROLSEX);
      ICC.dwICC  = ICC_LISTVIEW_CLASSES;
      InitCommonControlsEx(&ICC);

      // リストビュウ作成
      hCtlListView = CreateWindowEx(WS_EX_CLIENTEDGE,
                                    WC_LISTVIEW,
                                    NULL,
                                    WS_CHILD | WS_VISIBLE | LVS_REPORT,
                                    10,
                                    30,
                                    380,
                                    320,
                                    hWnd,
                                    0,
                                    hInstance,
                                    NULL);
      for (i = 0; i < (sizeof(LVColumn) / sizeof(LV_COLUMN)); i++)
      {
        ListView_InsertColumn(hCtlListView, i, &LVColumn[i]);
      }
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    default:
      return (DefWindowProc(hWnd, uMsg, wParam, lParam));
  }

  return 0L;
}

/* -------------------------------------------------------------------------- */
/*          WINAPI      WinMain                                               */
/*          PARAM       HINSTANCE   hInstance   : 現在のインスタンス          */
/*                      HINSTANCE   hPrevInst   : 直前のインスタンス          */
/*                      LPSTR       lpCmdLine   : コマンドライン引数          */
/*                      int         nCmdShow    : ウインドウの表示形式        */
/*          RETURN      int                     :                             */
/*                      メッセージの wParam パラメータ                        */
/*          REMARK      Win32 アプリケーションの初期エントリポイント          */
/*                      Windows システムが呼び出す。                          */
/* -------------------------------------------------------------------------- */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  /* 変数宣言部 */
  HWND              hWnd;
  WNDCLASSEX        WndClass;
  MSG               Msg;


  /* 関数本体部 */
  // ウインドウクラス作成
  WndClass.cbSize        = sizeof(WndClass);
  WndClass.style         = CS_HREDRAW | CS_VREDRAW;
  WndClass.lpfnWndProc   = WndProc;
  WndClass.cbClsExtra    = 0;
  WndClass.cbWndExtra    = 0;
  WndClass.hInstance     = hInstance;
  WndClass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hCursor       = LoadCursor(NULL, IDC_ARROW);
  WndClass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
  WndClass.lpszMenuName  = MAKEINTRESOURCE(IDM_MENUMAIN);
  WndClass.lpszClassName = "FTPManager";

  // ウィンドウクラス登録
  if (! RegisterClassEx(&WndClass))
  {
    return FALSE;
  }

  // ウィンドウ作成
  hWnd = CreateWindow(WndClass.lpszClassName, // 登録されているクラス名
                      "FTPManager",              // ウィンドウ名
                      WS_OVERLAPPEDWINDOW,    // ウィンドウスタイル
                      CW_USEDEFAULT,          // ウィンドウの横方向の位置
                      CW_USEDEFAULT,          // ウィンドウの縦方向の位置
                      CW_USEDEFAULT,          // ウィンドウの幅
                      CW_USEDEFAULT,          // ウィンドウの高さ
                      NULL,                   // 親ウィンドウまたはオーナーウィンドウのハンドル
                      NULL,                   // メニューハンドルまたは子ウィンドウ ID
                      hInstance,              // アプリケーションインスタンスのハンドル
                      NULL);                  // ウィンドウ作成データ

  if (! hWnd)
  {
    return FALSE;
  }

  // ウィンドウの表示
  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  // メッセージループ
  while (GetMessage(&Msg, NULL, 0, 0))
  {
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
  }

  return (int) Msg.wParam;
}

本日のソース: FTPManager105(VC8).zip   FTPManager105(BCC).zip

※小言

皆さんは、変数の付け方にはどのようなこだわりがありますか?私は、ハンガリー記法を採用しています。
ハンガリー記法とは、本来の名前の前に小文字で、変数の型をシンボル化した文字を付加する記法です。
この特徴は、変数を見ただけで変数の型がわかりやすいということにあります。
構造体については、大文字の型に対して、大文字小文字混在の変数宣言をするようにしています。
また、「HWND hWnd;」 の様に決まった変数名があり、これはこのまま使っています。

が、最近はハンガリー記法はあまり推奨されなくなってきています。
しかも、今まで推奨してきたマイクロソフトがやめるべきだといい始めました。

一時期、変数名は省略形で何でも表記しているのがもてはやされる時代がありましたが、わかりにくいという理由で今度は、長い変数名が主流になってきています。
これまた、省略形は使わないといった理由で CurrentDirectory という長い変数名なっています。

結論から言えば、わかりやすければよいということになります。
見た目にわかりやすいというのが重要でしょう。
CurrentDirectory がわかりやすいか szCurDir がわかりやすいか、皆さん自身で判断してください。


Last Update 2014/01/31 21:26
[Index] [Prev] [Next]