アップダウンコントロールで小数、負数を扱う

ダイアログボックスのアップダウンコントロール(スピンコントロール)を使っていて
小数が扱えないことに不満を感じたことはありませんか?
「なんで最小単位が設定できねーんだー!」
と私なんかは思ったものですよ……。
まあ、簡単に基本機能を提供するのが役目でしょうからね、しょうがないんでしょうけど……トホホ。

なーんて、諦めてはいけません(笑)!
プログラマーたる者、無ければ作ればいいじゃないか!という心構えが大切です。
と言うより、無いモノを作るのがプログラマーの役目です。
ただし、あるモノは使えばいいじゃないか!という心構えも現代プログラマーには必要です(本当)。

さて、本題に戻ります。
今回の課題は以下の通りです。

ダイアログボックスのアップダウンコントロール(スピンコントロール)で小数を扱えるようにする!

また、処理をお任せした場合の負数の取り扱い方法についても述べます。
今までは処理をお任せしていたはずですが、
その場合は負数の取り扱いに注意が必要なんですね。
こちらはおまけですので、本題とは関係ありません。

ちなみに、処理をお任せするとは
Set Buddy Integer スタイルを FALSE から TRUE にした場合のことです。

//////////////////// 小数を扱う ////////////////////

基本的な考え方はこうです。

お任せしないで自分で処理する!

ところで、今これを読んでいるあなたはずーっとお任せしてきたはずです。
もし自分で処理した経験があるのならば
小数を扱う方法も自然と分かるはずだからです。
はい、そろそろ気付いたんじゃないですかー(笑)?
そうです、自分で手取り足取り処理の流れを記述してやれば
どんなアップダウンコントロールでも作れるはずですね。
面倒くさそうだー。
いやいや、ご安心を。
分かってしまえば結構簡単に出来ます。
お任せする時の処理だってそれなりに複雑ですし、
むしろ自分でやった方がすっきりしてる気がします。
しかも可能性は無限大です!
是非ともマスターしましょう!

*** プログラムの仕様 ***

これから作るプログラムの仕様は以下のようにします。

ダイアログボックス
 *アップダウンコントロール
  ・クリックする毎に±0.01
 *エディットコントロール
  ・アップダウンコントロールと連動
  ・直接入力も可能
  ・初期値は前回設定した値
メインウィンドウ
 ・ダイアログボックスで設定した値を表示

実行するとこーんな感じです。
アップダウンコントロールの見た目を格好良くするために
Static Edge スタイルのみ FALSE から TRUE にしましたが、他はそのままです。



MYDLG DIALOGEX 0, 0, 186, 49
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION |
  WS_SYSMENU
CAPTION "アップダウンコントロール"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
  EDITTEXT        IDC_EDIT1,12,12,96,14,ES_AUTOHSCROLL
  DEFPUSHBUTTON  "OK",IDOK,129,7,50,14
  PUSHBUTTON     "キャンセル",IDCANCEL,129,24,50,14
  CONTROL        "",IDC_SPIN1,"msctls_updown32",UDS_ARROWKEYS,108,12,11,
                14,WS_EX_STATICEDGE
  LTEXT           "実数を入力して下さい",IDC_STATIC,12,30,71,8
END


*** メインウィンドウの処理を記述する ***

それでは、準備オッケーですね!?
先ずはメインウィンドウのプログラムを書いてしまいましょう。

#include<windows.h>
#include<stdio.h>
#include<commctrl.h>
#include"resource.h"


LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
ATOM InitApp(HINSTANCE);
BOOL InitInstance(HINSTANCE,int);
LRESULT CALLBACK MyDlgProc(HWND hDlg,UINT msg,WPARAM wp,LPARAM lp);

char szClassName[]="UpDownControl";
double dlg_d;

int WINAPI WinMain(HINSTANCE hCurInst,HINSTANCE hPrevInst,
  LPSTR lpsCmdLine,int nCmdShow)
{
  MSG msg;
  BOOL bRet;

  if(!InitApp(hCurInst))
    return FALSE;
  if(!InitInstance(hCurInst,nCmdShow))
    return FALSE;
  while((bRet=GetMessage(&msg,NULL,0,0))!=0){
    if(bRet==-1){
      MessageBox(NULL,"GetMessageエラー","Error",MB_OK);
      break;
    }else{
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }
  return (int)msg.wParam;
}

ATOM 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=(HICON)LoadImage(NULL,
    MAKEINTRESOURCE(IDI_APPLICATION),
    IMAGE_ICON,
    0,0,
    LR_DEFAULTSIZE|LR_SHARED);
  wc.hCursor=(HCURSOR)LoadImage(NULL,
    MAKEINTRESOURCE(IDC_ARROW),
    IMAGE_CURSOR,
    0,0,
    LR_DEFAULTSIZE|LR_SHARED);
  wc.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.lpszMenuName=NULL;
  wc.lpszClassName=(LPCTSTR)szClassName;
  wc.hIconSm=(HICON)LoadImage(NULL,
    MAKEINTRESOURCE(IDI_APPLICATION),
    IMAGE_ICON,
    0,0,
    LR_DEFAULTSIZE|LR_SHARED);

  return RegisterClassEx(&wc);
}

BOOL InitInstance(HINSTANCE hInst,int nCmdShow)
{
  HWND hWnd;

  hWnd=CreateWindow(szClassName,
    "アップダウンコントロール",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    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)
{
  HDC hdc;
  PAINTSTRUCT ps;

  
char str[32];
  HINSTANCE hInst;


  switch(msg){
    case WM_PAINT:
      hdc=BeginPaint(hWnd,&ps);
      
sprintf(str,"%lf",dlg_d);
      TextOut(hdc,0,0,str,(int)strlen(str));

      EndPaint(hWnd,&ps);
      break;
    
case WM_KEYDOWN:
      if(wp==VK_SHIFT){
        hInst=(HINSTANCE)GetWindowLong(hWnd,GWL_HINSTANCE);
        if(DialogBox(hInst,"MYDLG",hWnd,(DLGPROC)MyDlgProc)==IDOK)
          InvalidateRect(hWnd,NULL,FALSE);
      }
      break;

    case WM_CLOSE:
      DestroyWindow(hWnd);
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    default:
      return DefWindowProc(hWnd,msg,wp,lp);
  }
  return 0;
}


この辺はいつも通りですね。
赤字にしたところに注目して下さい。
OKボタンが押された場合、グローバル変数 dlg_d はダイアログボックスプロシージャで
直接書き換えてしまうので、その時のメインウィンドウの処理は再描画を指示するだけです。
キャンセルボタンが押された場合、グローバル変数 dlg_d は書き換えません。
ダイアログボックスと同期をとりたい場合はこのようにグローバル変数を使いましょう。

*** ダイアログボックスの処理を記述する ***

ここからが本題!
ダイアログボックスプロシージャの処理内容を細かく見ていきましょう。

〜初期化処理〜

static HWND hEdit1,hUpdown1;
double d;
char str[32];

case WM_INITDIALOG:
  d=dlg_d;

  hEdit1=GetDlgItem(hDlg,IDC_EDIT1);
  hUpdown1=GetDlgItem(hDlg,IDC_SPIN1);

  sprintf(str,"%lf",d);
  SetWindowText(hEdit1,str);
  return TRUE;


double型変数 d にグローバル変数 dlg_d の値をコピーします。
グローバル変数 dlg_d の値を書き換えるのはOKボタンが押された時だけ。
つまり、グローバル変数 dlg_d には前回OKボタンを押した時の値が保存されているはずです。
前回の値が取得できたら、これを初期値としてエディットウィンドウに表示させましょう。

〜OKボタン、キャンセルボタンが押された時〜

case WM_COMMAND:
  switch(LOWORD(wp)){
    case IDOK:
      GetWindowText(hEdit1,str,(int)sizeof(str));
      dlg_d=atof(str);
      EndDialog(hDlg,IDOK);
      return TRUE;
    case IDCANCEL:
      EndDialog(hDlg,IDCANCEL);
      return TRUE;
  }
  return FALSE;


OKボタンが押された場合は、
エディットウィンドウから文字列を取得→double型に変換→グローバル変数を書き換えます。
エディットウィンドウには最終的な値(文字列)が保持されているはずですね。
しかし、なぜこのタイミングで数値を取得しなければならないのか?
だってリアルタイムにエディットウィンドウに表示させるんでしょ?
ということはリアルタイムに値を取得しているのでは?
なかなか鋭い質問ですが、これについては後で述べます。

〜重要!アップダウンコントロールが押された場合〜

LPNMUPDOWN lpnmud;

case WM_NOTIFY:
  if(wp==(WPARAM)IDC_SPIN1){
    lpnmud=(LPNMUPDOWN)lp;
    if(lpnmud->hdr.code==UDN_DELTAPOS){
      GetWindowText(hEdit1,str,(int)sizeof(str));
      d=atof(str);
      if((lpnmud->iDelta)<0) d+=0.01;
      else if((lpnmud->iDelta)>0) d-=0.01;
      sprintf(str,"%lf",d);
      SetWindowText(hEdit1,str);
      return TRUE;
    }
  }
  return FALSE;


知りたいのは上矢印が押されたのか、下矢印が押されたのか、という情報ですが、
これを直接知るすべはありません(ありそうな気もするが……)。
しかし、NMUPDOWN構造体のiDeltaメンバーには
上矢印が押された場合は負の値が、
下矢印が押された場合は正の値が入ります。

NMUPDOWN構造体の定義は次のようになっています。

typedef struct _NM_UPDOWN {
  NMHDR hdr;  //NMHDR構造体
  int iPos;     //古い値
  int iDelta;    //差分(新しい値-古い値)
} NMUPDOWN, FAR *LPNMUPDOWN;


iDelta の値に注意して下さい。
新しい値は iPos+iDelta です。
これらの事実より、上矢印ダウンボタン
下矢印アップボタンとして機能していることが分かります。

NMHDR構造体の定義は次のようになっています。

typedef struct tagNMHDR {
  HWND hwndFrom;  //メッセージを送ってきたコントロールのウィンドウハンドル
  UINT idFrom;     //メッセージを送ってきたコントロールのID
  UINT code;      //メッセージ
} NMHDR;


ところで、アップダウンコントロールが押された場合、
どんなメッセージが送られて来るんでしょう?
次の順番でメッセージは送られてきます。

UDN_DELTAPOS  アップダウンコントロールの値が変わろうとしている
 
WM_VSCROLL    アップダウンコントロールの値が変わった

UDN_DELTAPOS は WM_NOTIFY の形式で送られて来ます。

UDN_DELTAPOS メッセージは以下の副メッセージを保持しています。

UDN_DELTAPOS
  lpnmud = (LPNMUPDOWN) lParam;  //NMUPDOWN構造体へのポインタ


WM_NOTIFY メッセージは以下の副メッセージを保持しています。

WM_NOTIFY
  idCtrl = (int) wParam;       //メッセージを送ってきたコントロールのID
  pnmh = (LPNMHDR) lParam;  //NMHDR構造体へのポインタ


というわけで、UDN_DELTAPOS メッセージが送られて来た時、
副メッセーの内容は以下のようになっていますね。

idCtrl = (int) wParam;       //メッセージを送ってきたコントロールのID
lpnmud = (LPNMUPDOWN) lParam;  //NMUPDOWN構造体へのポインタ

おまけに、WM_VSCROLL メッセージは以下の副メッセージを保持しています。

WM_VSCROLL
  nScrollCode = (int) LOWORD(wParam);  //通知メッセージ(ユーザーはどんな操作をしたか?)
  nPos = (short int) HIWORD(wParam);   //新しい値
  hwndScrollBar = (HWND) lParam;      //メッセージを送ってきたコントロールのウィンドウハンドル


アップダウンコントロールが内部に保持していると思われる値は無視するとして、
とにかく上矢印が押されたのか下矢印が押されたのかが分かったら
任意の値を加えるか減らすかしてやりましょう。
古い値はエディットウィンドウに表示されている文字列です。
エディットウィンドウの文字列は放っておいても消えることはありませんから、
古い値が今でも保持されているはずです。
また、UDN_DELTAPOS メッセージを利用するか WM_VSCROLL メッセージを利用するのかも
悩み所かも知れません。
しかし、NMUPDOWN構造体を連れてきてくれるのは UDN_DELTAPOS メッセージだけです。
従って、UDN_DELTAPOS メッセージを利用しましょう。
ただ一般的には、UDN_DELTAPOS メッセージは本当に値を上下させるのかの判断に用いて、
WM_VSCROLL メッセージで表示を更新するような流れが王道みたいです。

〜エディットコントロールに直接入力された場合〜

何もする必要なし!
放っておいたら入力した文字列が勝手に消えてしまうなんて事もありませんしね。
ただし、このプログラムでは数値と解釈できない文字列を入力した時の動作は保証できません。
また、入力する桁数が多すぎる場合も動作保証対象外です。
文字列を取得している GetWindowText(hEdit1,str,(int)sizeof(str)); 関数の定義は次のようになります。

int GetWindowText(
  HWND hWnd,     // ウィンドウまたはコントロールのハンドル
  LPTSTR lpString,  // テキストバッファ
  int nMaxCount    // コピーする最大文字数
);

nMaxCount:バッファにコピーする文字の最大数を指定します。
        テキストのこのサイズを超える部分は、切り捨てられます。
        NULL 文字も数に含められます。


ところが、実際には切り捨ては行われないようです。
この関数のエラーを捕まえてもダメ。
下記に示したエラーメッセージを見ても分かるように、テキストバッファのオーバーフローですね。

Run-Time Check Failure #2 - Stack around the variable 'str' was corrupted.

従って、テキストバッファはかなり大きく確保することが重要です。
あるいはその都度メモリを確保するのも良いかもしれませんが、
今回の場合、WM_INITDIALOG でdouble型変数から文字数を調べるのが面倒なのでやめました。
ちなみに、32バイト用意した今回の場合、30文字の実数が入力できます(整数+小数)。
残り2文字は小数点とヌル文字が使います。

〜全体の流れ〜

数値の取得はアップダウンコントロールが押された場合、
OKボタンが押された場合に行います。
ちょっと考えただけではOKボタンが押された時に取得するのは二度手間では?
と思うかも知れませんが、
エディットコントロールに直接入力された場合は何も処理しないこのプログラムでは
もしかして最後にエディットコントロールに数値(文字列)を直接入力した場合、
値が取得されていないことになります。
従って、OKボタンが押された時にも値を取得する必要があるのです。
また、現在の値は変数に保存するのではなく、
エディットコントロールに文字列として保存させているのがポイントです。

それでは実行してみましょう。



Shiftキーを押すと……



OKボタンを押すと……



小数点以下は6桁しか表示されませんが、
何桁でも入力は可能ですし、データが失われることもありません。
表示桁数を増やしたい場合は以下に示す sprintf()関数の %lf を書き換えるだけです。
ただし、精度はあまり保証されません。

sprintf(str,"%lf",d);
sprintf(str,"%lf",dlg_d);


完成版プログラムはこちらです。
実行ファイルも含めた全ファイルはこちらからダウンロードして下さい。

このプログラムをコンパイルすると以下の警告文が出ますが、
これは64ビット環境に移植した際に問題になるよーという意味です。
32ビット環境の今では 32ビット型変数の LONG より大きな型は無いはずですからね。

warning C4312: '型キャスト' : 'LONG' からより大きいサイズの 'HINSTANCE' へ変換します。

//////////////////// お任せした場合の負数の取り扱い ////////////////////

設定可能な範囲に負数を含めた場合でも
エディットウィンドウの表示は正常に行われるため、
一見正常に動いているように見えます。
しかし、設定した値をメインウィンドウで表示してやると以下のようになります。

エディットウィンドウ メインウィンドウ

          -2 65534
          -1 65535
           0 0
           1 1

どうやらアップダウンコントロールの扱える値は 0〜65535 であることが分かります。
どこかでそんな記事を読んだ気もします。
そうなると何故エディットコントロールの表示は正常なのかという疑問が湧いてきますが、
正直私にも分かりません(汗)。

ところで、スライダーコントロールは負数も正常に扱えました。
……謎ですね。
つまり、疑わしい処理結果が出るようならダイアログボックスで設定した値を
表示させてみろ……と、そういうことですか。

ここで、負数が2の補数で表現される3ビット型について考えてみましょう。
ビットの状態と10進数を並べて書くと以下のようになります。

000  0
001  1
010  2
011  3
100 -4
101 -3
110 -2
111 -1

ところが、負数が扱えないシステムがこれを解釈すると以下のようになります。

000 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

このように、本当は -4 を表現しているビットが 4 と解釈されてしまいました。
これを本当の値に修正するためにはどうすればよいでしょうか?
よく見ると法則があるのが分かります。
解釈された値が 3 を越えた場合は、解釈された値-8 が本当の値になりますね。
例えば、4 と解釈された場合は 4-8=-4 で、本当に値に修正されましたね。
修正値 8 は2を3乗したものです。

000  0
001  1
010  2
011  3
100 -4 =4-8
101 -3 =5-8
110 -2 =6-8
111 -1 =7-8

同様に、ダイアログボックスの場合も符号付き16ビット型の最大値 32767 を越えた場合は、
誤って解釈されていることになるので、65536 を引いてやれば本当に値に修正されます。
以下にOKボタンが押された時の処理を示します。

if(x>32767) dlg_x=x-65536;
else dlg_x=x;


x : ダイアログボックスプロシージャ内の変数
dlg_x : 結果を保存するグローバル変数

もちろん、小数を扱う時のように全てを自分で処理した場合は関係のない話です。


戻る / ホーム