4.標準コントロール(2) 〜 コンボボックス(2) 〜

 

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

 


1.コンボボックスにドライブ一覧を表示する

さて、めでたく(?)コンボボックスにカレントディレクトリを表示することはできましたが、矢印を押しても何も出てきませんね。
これはちょっと寂しいです。
それで、ここに選択可能な(現在有効な)ドライブ一覧を表示したいと思います。

ここまでくると、ドライブ一覧を取得する関数があるのかな?って思いますよね。
その通りですね。 ドライブ一覧取得するには、 GetLogicalDriveStrings() を使用します。


DWORD GetLogicalDriveStrings(

  DWORD nBufferLength,  // バッファのサイズ

  LPTSTR lpBuffer       // ドライブの文字列を格納するバッファ

);

これも、GetCurrentDirectory() と同じように格納するバッファとそのサイズを入れます。
何か文字列を返す関数は、このようになっているのが多いようです。
そういうものかと思うことが重要です。こうしてだんだんあたりがつくようになるのです。(たぶん)

GetLogicalDriveStrings() が成功した場合には、ドライブの文字列がバッファにセットされているのですが、ちょっと変なことに、格納される文字列は、

A:\<null>C:\<null>D:\<null><null>
※<null> は終端の NULL 文字 ”\0” を表す

というふうになります。通常Cの文字列(charの配列)は NULL文字 ”\0” によって終了していますが、これは、格納された文字列の中に出現します。
また最後に2つ連なっています。
なぜ、このような妙な格納の仕方をしているのかというと、Cでの文字列(charの配列)の扱いと深い関係があるのです。

通常、Cでは文字列を扱えませんので、charの配列を文字列のように扱っています。(実際はcharの配列の先頭要素へのポインタですね。う〜ん、なんかややこしい)
そうして、charの配列は、NULL文字 ”\0” で終了します。
このことを使って、通常はそんなに意識せずに文字列を扱っています。

ex. strcpy(arrayA, arrayB)  BをAにコピーする

この場合、arrayBはcharの配列ですが、式の中では配列は「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられています。
配列は、メモリ上も連続した領域に格納されるので、システム側で次の要素をメモリ上から取得できます。
そうしてそれを NULL 文字 (\0) が出てくるまで行い、出てきた時点でそれまでに読んだ内容で文字列を組み立てます。

先ほどの GetLogicalDriveStrings() で返される文字列の配列には、途中にNULL 文字 ”\0” が出てくるので、文字列としてはそこで終了してしまいます。
ここで、重要なのは、NULL 文字 が出てくると、その後になにが入っていようが、NULL文字 以降は無視されるということです。
つまり strcpy(arrayA, arrayB); を行うと、"A:\" のみが arrayA にコピーされ、NULL文字以降の "C:\<null>D:\<null><null>" は無視されます。
では、「配列の先頭要素へのポインタ」にいつも自動で読み替えられる配列自体を指定せずに、配列の各文字列の先頭要素を指定した場合はどうなるでしょうか?

この場合は、0番目、4番目、8番目ですね。それぞれのNULL文字までを文字列として認識しますので、3、7、11番目までを文字列として取得します。
strcpy(arrayA1, &arrayB[0]);
strcpy(arrayA2, &arrayB[4]);
strcpy(arrayA3, &arrayB[8]);
と書けば、めでたくすべてのドライブを取得できます。

さて、このドライブを取得するロジックを組み込みましょう。




LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

  /* 変数宣言部 */

  HINSTANCE         hInstance;

  HWND              hCtlCombo;

  char              szCurDir[MAX_PATH + 1];

  char              szDrives[128];

  int               i;





  /* 関数本体部 */

  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++;

      }

      break;

    case WM_DESTROY:

      PostQuitMessage(0);

      break;

    default:

      return (DefWindowProc(hWnd, uMsg, wParam, lParam));

  }



  return 0L;

}



どうですか?
なるほどって思っていただけましたか?私はこのコードを見たときになんて エレガント なんだろうと思いました。
まさに、このような書き方ができるように、A:\<null>C:\<null>D:\<null><null> という奇妙な形になっているのですね。

前回使っていた、SetWindowText(hCtlCombo, szCurDir) を ComboBox_SetText(hCtlCombo, szCurDir) に変更しました。
このマクロを使うために、WindowsX.h をインクルードしてください。

コンボボックスの下部のリストに項目を追加するには CB_ADDSTRING メッセージを使います。
SendMessage(hCtlCombo, CB_ADDSTRING, 0L, (LPARAM) &szDrives[i]) と書きますが、今回はマクロ ComboBox_AddString を使用して記述しています。

以下のように、ちゃんとドライブレターが取得できました。

このコンボボックスには、CBS_SORT が指定してあるので(CreateWindow() の第3引数です)自動でアルファベット順に並びました。


2.ポインタ演算ってなんだ?

実は、このドライブを取得するロジックはこんな書き方をすることがあります。




  char              szDrives[128];

  char *            p;

  int               i;



  GetLogicalDriveStrings(sizeof(szDrives), szDrives);



  // ポインタ変数に配列のアドレスを代入

  p = szDrives;



  i = 0;

  while (*p)

  {

    SendMessage(hCtlCombo, CB_ADDSTRING, 0L, (LPARAM) p);



    // 次のドライブ名へ

    while (*p)

    {

      p++;

    }



    p++;

  }



これは、ポインタ演算といって、いったん配列をポインタにセットしてから処理を行おうというものです。
ちょっと意味がわかりにくいので、こういった書き方は通(玄人さんのことです)の人しかしないようです。

一応説明しますと、配列は、[ ] の記号の有無にかかわらず、「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられるため、さきほどの
strcpy(arrayA1, &arrayB[0]);
strcpy(arrayA2, &arrayB[4]);
strcpy(arrayA3, &arrayB[8]);
と書いたarrayBもすべて「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられています。
じゃあ、[4] って書いたときにはどうなっているんだといいますと、[ ]をつけた場合、そのポインタ(アドレス)に [ ] 中の値だけ加算されたアドレスを返しているのです。
そうしてさらにその加算されたアドレスの指しているデータを返すのです。
実際にデータを表示してみましょう。

この場合、2行目の szDrives は、配列ですから「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられているので、値が 0x0012f4d8 というポインタになっていますね。
4行目を見てください。これは、まず「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられたあと、[ ] 中の値だけポインタが進みます。
つまり 5 (配列の要素は 0 からカウントされていますので、i = 4 は 5) 進んでいます(ここでいうと 0x0012f4dc です)。
さらに、アドレスの指しているデータを返すので、 C という文字が値として返されます。
strcpy にはポインタを渡さないといけないため、szDrives[i] に再度 & をつけてポインタにしているのです。
演算子の優先順序を見るとわかりますが、先に[ ]演算子が働きます。そして[ ]演算子が機能した後は、せっかくポインタになっていたものが、データそのものになっています。
このため & をつけて再度ポインタを取得しなければいけないのです。

つまり、&szDrives[4] は、どう理解すればいいかというと
@ szDrives が「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられる。
A [ ] 演算子により、そのポインタ(アドレス)に 5 加算され、指しているデータを返す。
B & 演算子により再度指しているデータのポインタ(アドレス)を取得する。
と 実はポインタ → データ → ポインタとまどろっこしいことをやっているのでした。

[ ] 演算子 を誤解されている方がいらっしゃいますが、ポインタ(アドレス)に[ ] 中の値だけ加算し、そのデータを返す意味しかなく、本来は配列とは関係ありません。
ですが、ややこしいことに、宣言のときの [ ] は配列宣言子です。
つまり配列そのものです。
C言語では、似た感じの使い方がある場合、同じ記号を使いたがる傾向があり、理解の妨げになることが、ままあります。

[ ] がポインタを引数にとるということは、何も配列である必要はありません。
ポインタそのものでもいいことになります。
このため、以下の書き方もできるようになります。




  char              szDrives[128];

  char *            p;

  int               i;



  GetLogicalDriveStrings(sizeof(szDrives), szDrives);



  // ポインタ変数に配列のアドレスを代入

  p = szDrives;



  i = 0;

  while (p[i] != '\0')

  {

    SendMessage(hCtlCombo, CB_ADDSTRING, 0L, (LPARAM) &p[i]);



    // 次のドライブ名へ

    while (p[i] != '\0')

    {

      i++;

    }



    i++;

  }



この場合、p = szDrives; の記述で一度だけ、@ szDrives が「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられる処理が行われ、ループの中では、A と Bしか行われません。

さらに、ポインタはそれ自体計算が可能で、加算が行われた場合、ポインタが指し示す方の分だけアドレスが進みますので [ ] 演算子と同じようなことをします。
両者の違いは計算結果が、アドレスのままか、アドレスの指すデータかの違いになりますね。
そうすると、先に書いたような書き方ができるようになります。




  char              szDrives[128];

  char *            p;

  int               i;



  GetLogicalDriveStrings(sizeof(szDrives), szDrives);



  // ポインタ変数に配列のアドレスを代入

  p = szDrives;



  i = 0;

  while (*p)

  {

    SendMessage(hCtlCombo, CB_ADDSTRING, 0L, (LPARAM) p);



    // 次のドライブ名へ

    while (*p)

    {

      p++;

    }



    p++;

  }



この場合、p = szDrives; の記述で一度だけ、@ szDrives が「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられる処理が行われ、その後は、ポインタ(アドレス)のまま計算されています。
そして、SendMessage() の引数にもポインタ(アドレス)のまま渡されています。
while (*p) のときのみ * でそのデータを取得して NULL文字 かどうかを判断しているのでした。
NULL文字 は ”\0” のことですが、実際の値は ”0”です。このため while (*p != '\0') を while (*p) と記述しています。

確かに効率はいいようですが、「一見してわかりにくいように見える」ので私は使用しませんが、読まなければいけない場面に遭遇するかも知れません。

またまた、くどくど説明してしまいました。
理解できなくもかまいませんのでどんどん先に読み進めてください。

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




/* ========================================================================== */

/*          FILE        WinMain.cpp                                           */

/*          CREATE      2006/10/01                                            */

/*          MODIFY      2006/12/01                                            */

/*          REMARK                                                            */

/*          COPYRIGHT   2000-2007, Latin. All Rights Reserved                 */

/* ========================================================================== */

#include <Windows.h>

#include <WindowsX.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;

  char              szCurDir[MAX_PATH + 1];

  char              szDrives[128];

  int               i;





  /* 関数本体部 */

  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++;

      }

      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;

}



リストの中に、ドライブレターとカレントディレクトリを入れています。
なお、コンボボックスの作成時に高さを小さくしすぎると、リストが現れません。試して見てください。

また、式の中では配列は「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられると書きましたが、sizeof(szDrives) の場合、「配列の先頭要素へのポインタ(アドレス)」に自動で読み替えられては、ポインタのサイズは取得できても配列の要素数は取得できませんよね。
sizeof のときは、例外的に「配列の先頭要素へのポインタ」にならず配列そのものが渡されます。

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


※小言

C言語の聖書?であるK&Rと呼ばれている「プログラミング言語C」(共立出版株式会社)には、「ポインタを使うほうが一般に高速である。」と記述されています。
ここら辺は、苦しんで覚えるC言語(苦C) のコラム:「C言語の聖書? K&R」に記述がありますので参照してください。

C言語は、人間が書く言語です。コンピュータが直接使う言語(2進数)ではありません。
つまり、人間がわかりにくい言語では意味がありません。特に複雑な処理を行う以外は、ポインタ演算は使用しないようにしましょう。
少なくとも私はそうしますが、他人が書いたプログラムを読む場合にはそうも言ってられませんので、そういう方言も存在することくらいは頭の片隅においておきましょう。


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