まず、時計の針の表示方法ですが次のように考えると簡単です。
1.針は中心から、針の先端までの線分である
2.奥にあるものから順に描画する(文字盤、短針、長針、秒針の順)
時計の中心は(CLOCK_HEIGHT / 2, CLOCK_HEIGHT / 2)ですね。 これを簡単のために(x0, y0)とします。
さて、針の先端の座標はどうなるのでしょうか。針の長さをLとすると
x = x0 + L * sin(A) y = y0 - L * cos(A)となります。(三角関数を忘れてしまった人は昔の教科書を引っ張り出してみてください。) 学校で習う座標とちょっと異なるのは、y座標は下に行くほど値が大きくなる点です。
Aは12時の方向から時計回りに測った針の角度です。また、プログラムで使う角度は ラジアン単位であることに注意してください。(πラジアン=180度)
さて、現在h時m分s秒であるとします。この時秒針の先端の座標はどうなっているでしょうか。 60秒で360度ですからs秒では角度は
s * 6度
ですね。これをラジアン単位に直すと
s * 6 * PAI / 180 = s * PAI / 30
となりますね。(上の式の=は数学の=の意味です。PAIはπです)
従ってs秒の時の秒針の座標は
x = x0 + L * sin(s * PAI / 30) y = y0 - L * cos(s * PAI / 30)となります。(x0, y0)から上の座標に直線を引けばよいですね。
次は、短針について考えます。h時は24時間制なのでhが12以上の時は12を引いて考えます。 12時で360度なので1時では30度です。ラジアン単位に直すと
h * 30 * PAI / 180 = h * PAI / 6
はい、毎正時の時はこれでよいのですが、たとえば2時30分の時はどうでしょうか。短針は 2と3の中間を指しているはずです。つまり、分の値も考慮しなくてはいけません。
60分で長針は30度進みます。1分では1/2度進みます。ラジアン単位ではPAI / 360ですね。 本当は秒も考慮しなくてはいけないのですが、誤差範囲なので省略します。
h時m分の時の短針の座標は
x = x0 + L * sin(h * PAI / 6 + m * PAI / 360) y = y0 - L * cos(h * PAI / 6 + m * PAI / 360)次は長針について考えてみます。
60分で360度なのでm分では角度は
m * 6度
ラジアン単位にすると
m * 6 * PAI / 180 = m * PAI / 30
ですね。また、60秒で長針は6度進みます。1秒で1 / 10度進みます。 ラジアン単位ではPAI / 1800ですね。
従ってm分s秒の時の長針の座標は
x = x0 + L * sin(m * PAI / 30 + s * PAI / 1800) y = y0 - L * cos(m * PAI / 30 + s * PAI / 1800)となります。
では、プログラムを見てみましょう。
リソーススクリプトは、変更はありません。
// clock04.cpp #define hParentKey HKEY_CURRENT_USER #define lpszSubKey "Software\\Kumei\\Clock" #define PAI 3.14159 #define CLOCK_WIDTH 250 //時計全体のウィンドウ幅 #define CLOCK_HEIGHT 60 //高さ #define MYTIMER 1 //タイマーID #include <windows.h> #include <math.h> #include "resource.h" typedef struct MYDATA { COLORREF cr_bg; COLORREF cr_txt; int x; int y; } INIDATA; LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); ATOM InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); HFONT SetMyFont(LPCTSTR, int); BOOL GetMyColor(HWND, COLORREF *, COLORREF); void GetInitialSettings(INIDATA *); BOOL GetDataDWORD(char *, DWORD *); BOOL SetInitialSettings(INIDATA); BOOL SetDataDWORD(char *, DWORD); BOOL ShowClock(HDC, char *); BOOL GetHMS(char *, int *, int *, int *); char szClassName[] = "clock04"; //ウィンドウクラス char szAppName[] = "猫クロック"; //アプリケーション名 HINSTANCE hInst;πの値をPAIでdefineしました。
また、三角関数使うのでmath.hをincludeする必要があります。
GetMyColor関数の引数をちょっと変えてみました。3番目の引数に 現在の色を指定します。どうしてこのようにしたかというと、今までは 文字色を指定した後、背景色を指定しようとするとデフォルトで選択されている 色が文字色であるからです。これを解消するための仕様です。ただし、カスタマイズした 色については、無効です。(もちろん方法はありますが、今回は簡単のため省略)
前回までszBuf, szBuf2はグローバル変数でしたが、あまり意味がないので ローカル変数にしました。
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; BOOL bRet; hInst = hCurInst; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) { if (bRet == -1) { 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 = (LPCSTR)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, "猫でもわかるWindowsプログラミング", //タイトルバーにこの名前が表示されます WS_POPUP, //ウィンドウの種類 0, //X座標 0, //Y座標 CLOCK_WIDTH, //幅 CLOCK_HEIGHT, //高さ 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, x, y; SYSTEMTIME st; PAINTSTRUCT ps; HDC hdc; HFONT hFont; SIZE s; HRGN hRgn, hRgn1, hRgn2, hRound1Rgn, hRound2Rgn, hRectRgn; HBRUSH hBrush; HMENU hMenu, hSubMenu; POINT pt; COLORREF cr; static INIDATA inidata; RECT rc; char szYobi[8]; static char szBuf[64], szBuf2[64]; //時刻表示用 switch (msg) { case WM_CREATE: GetInitialSettings(&inidata); MoveWindow(hWnd, inidata.x, inidata.y, CLOCK_WIDTH, CLOCK_HEIGHT, TRUE); SetTimer(hWnd, MYTIMER, 500, NULL); hRgn = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, 1, 1); hRgn2 = CreateRectRgn(0, 0,1, 1); hRound1Rgn = CreateEllipticRgn(0, 0, CLOCK_HEIGHT, CLOCK_HEIGHT); hRectRgn = CreateRectRgn(CLOCK_HEIGHT / 2, 0, CLOCK_WIDTH - CLOCK_HEIGHT / 2, CLOCK_HEIGHT); CombineRgn(hRgn1, hRound1Rgn, hRectRgn, RGN_OR); hRound2Rgn = CreateEllipticRgn(CLOCK_WIDTH - CLOCK_HEIGHT, 0, CLOCK_WIDTH, CLOCK_HEIGHT); CombineRgn(hRgn2, hRound2Rgn, hRectRgn, RGN_OR); CombineRgn(hRgn, hRgn1, hRgn2, RGN_OR); SetWindowRgn(hWnd, hRgn, TRUE); DeleteObject(hRound1Rgn); DeleteObject(hRound2Rgn); DeleteObject(hRectRgn); DeleteObject(hRgn1); DeleteObject(hRgn2); break; case WM_RBUTTONDOWN: pt.x = LOWORD(lp); pt.y = HIWORD(lp); hMenu = LoadMenu(hInst, "MYMENU"); hSubMenu = GetSubMenu(hMenu, 0); ClientToScreen(hWnd, &pt); TrackPopupMenu(hSubMenu, TPM_LEFTALIGN, pt.x, pt.y, 0, hWnd, NULL); DestroyMenu(hMenu); break; case WM_LBUTTONDOWN: PostMessage(hWnd, WM_NCLBUTTONDOWN, (WPARAM)HTCAPTION, lp); break; case WM_TIMER: if (wp != MYTIMER) return DefWindowProc(hWnd, msg, wp, lp); GetLocalTime(&st); wsprintf(szBuf, "%02d:%02d:%02d", st.wHour, st.wMinute, st.wSecond); switch (st.wDayOfWeek) { case 0: strcpy(szYobi, "Sun"); break; case 1: strcpy(szYobi, "Mon"); break; case 2: strcpy(szYobi, "Tue"); break; case 3: strcpy(szYobi, "Wed"); break; case 4: strcpy(szYobi, "Thu"); break; case 5: strcpy(szYobi, "Fri"); break; case 6: strcpy(szYobi, "Sat"); break; } wsprintf(szBuf2, "%d/%02d/%02d(%s)", st.wYear, st.wMonth, st.wDay, szYobi); InvalidateRect(hWnd, NULL, TRUE); break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); hBrush = CreateSolidBrush(inidata.cr_bg); SelectObject(hdc, hBrush); PatBlt(hdc, 0, 0, CLOCK_WIDTH, CLOCK_HEIGHT, PATCOPY); hFont = SetMyFont("MS ゴシック", 32); SelectObject(hdc, hFont); GetTextExtentPoint32(hdc, szBuf, (int)strlen(szBuf), &s); x = (CLOCK_WIDTH - s.cx) / 2 + 10; y = (CLOCK_HEIGHT - s.cy) / 2 + 10; SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, inidata.cr_txt); TextOut(hdc, x, y, szBuf, (int)strlen(szBuf)); DeleteObject(hFont); hFont = SetMyFont("MS ゴシック", 18); SelectObject(hdc, hFont); GetTextExtentPoint32(hdc, szBuf2, (int)strlen(szBuf2), &s); x = (CLOCK_WIDTH - s.cx) / 2 + 10; TextOut(hdc, x, 4, szBuf2, (int)strlen(szBuf2)); DeleteObject(hFont); DeleteObject(hBrush); ShowClock(hdc, szBuf); EndPaint(hWnd, &ps); break; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_END: SendMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_BACKGROUND: if (GetMyColor(hWnd, &cr, inidata.cr_bg)) { inidata.cr_bg = cr; } break; case IDM_TEXT: if (GetMyColor(hWnd, &cr, inidata.cr_txt)) { inidata.cr_txt = cr; } break; } break; case WM_CLOSE: id = MessageBox(hWnd, "終了してもよろしいですか", szAppName, MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { GetWindowRect(hWnd, &rc); inidata.x = rc.left; inidata.y = rc.top; DestroyWindow(hWnd); } break; case WM_DESTROY: SetInitialSettings(inidata); KillTimer(hWnd, MYTIMER); PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0; }szBuf, szBuf2をstaticなローカル変数にしました。
WM_PAINメッセージが来た時に日付、時刻の表示位置を少し右の方にずらしました。
また、日付・時刻描画後にShowClock関数を呼んでアナログ部分を描画します。
メニューからIDM_BACKGROUNDやIDM_TEXTが選択された時GetMyColor関数の最後の引数に 現在の色を指定するようにしました。
HFONT SetMyFont(LPCTSTR face, int h) { HFONT hFont; hFont = CreateFont(h, //フォント高さ 0, //文字幅 0, //テキストの角度 0, //ベースラインとx軸との角度 FW_REGULAR, //フォントの重さ(太さ) FALSE, //イタリック体 FALSE, //アンダーライン FALSE, //打ち消し線 SHIFTJIS_CHARSET, //文字セット OUT_DEFAULT_PRECIS, //出力精度 CLIP_DEFAULT_PRECIS,//クリッピング精度 PROOF_QUALITY, //出力品質 FIXED_PITCH | FF_MODERN,//ピッチとファミリー face); //書体名 return hFont; }この関数に変更はありません。
BOOL GetMyColor(HWND hWnd, COLORREF *lpcr, COLORREF org_cr) { CHOOSECOLOR cc; static DWORD dwCustColors[16]; cc.lStructSize = sizeof(CHOOSECOLOR); cc.hwndOwner = hWnd; cc.lpCustColors = dwCustColors; cc.rgbResult = org_cr; cc.Flags = CC_RGBINIT; if (ChooseColor(&cc)) { *lpcr = cc.rgbResult; return TRUE; } return FALSE; }ちょっとだけ変更しました。最後の引数は現在の色です。 これをcc.rgbResultに指定することでデフォルトの選択色になります。 カスタマイズした色についてもこのようにしたい時はdwCustColorsも全部 記録しておくと可能です。
void GetInitialSettings(INIDATA *lpini) { DWORD dwData; if (GetDataDWORD("background-color", &dwData)) { lpini->cr_bg = (COLORREF)dwData; } else { lpini->cr_bg = RGB(0, 255, 255); } if (GetDataDWORD("text-color", &dwData)) { lpini->cr_txt = (COLORREF)dwData; } else { lpini->cr_txt = RGB(0, 0, 0); } if (GetDataDWORD("x", &dwData)) { lpini->x = (int)dwData; } else { lpini->x = 0; } if (GetDataDWORD("y", &dwData)) { lpini->y = (int)dwData; } else { lpini->y = 0; } return; } BOOL GetDataDWORD(char *szName, DWORD *dwValue) { HKEY hKey; DWORD dwPosition; DWORD dwType = REG_DWORD; DWORD dwByte = 32; RegCreateKeyEx(hParentKey, lpszSubKey, 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, &dwPosition); if (RegQueryValueEx(hKey, szName, NULL, &dwType, (BYTE *)dwValue, &dwByte) != ERROR_SUCCESS) { RegCloseKey(hKey); return FALSE; } RegCloseKey(hKey); return TRUE; } BOOL SetInitialSettings(INIDATA inidata) { SetDataDWORD("background-color", inidata.cr_bg); SetDataDWORD("text-color", inidata.cr_txt); SetDataDWORD("x", inidata.x); SetDataDWORD("y", inidata.y); return TRUE; } BOOL SetDataDWORD(char *szName, DWORD dwData) { HKEY hKey; DWORD dwPosition; RegCreateKeyEx(hParentKey, lpszSubKey, 0, "", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, &dwPosition); RegSetValueEx(hKey, szName, 0, REG_DWORD, (CONST BYTE *)&dwData, sizeof(DWORD)); RegCloseKey(hKey); return TRUE; }これらの関数に変更はありません。
BOOL ShowClock(HDC hdc, char *lpszBuf) { HBRUSH hBrush; HPEN hPen; int h, m, s, x0, y0, x, y; int l = CLOCK_HEIGHT / 2; if (strcmp(lpszBuf, "") == 0) return FALSE; hBrush = CreateSolidBrush(RGB(0, 255, 255)); SelectObject(hdc, hBrush); Ellipse(hdc, 1, 1, CLOCK_HEIGHT - 1, CLOCK_HEIGHT - 1); Ellipse(hdc, 4, 4, CLOCK_HEIGHT - 4, CLOCK_HEIGHT - 4); DeleteObject(hBrush); GetHMS(lpszBuf, &h, &m, &s); x0 = y0 = CLOCK_HEIGHT / 2; //短針 if (h > 11) h -= 12; hPen = CreatePen(PS_SOLID, 4, RGB(0, 0, 255)); SelectObject(hdc, hPen); MoveToEx(hdc, x0, y0, NULL); x = (int)(x0 + (l - 16) * sin(h * PAI / 6 + m * PAI / 360)); y = (int)(x0 - (l - 16) * cos(h * PAI / 6 + m * PAI / 360)); LineTo(hdc, x, y); DeleteObject(hPen); //長針 hPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0)); SelectObject(hdc, hPen); MoveToEx(hdc, x0, y0, NULL); x = (int)(x0 + (l - 10) * sin(m * PAI / 30 + s * PAI / 1800)); y = (int)(y0 - (l - 10) * cos(m * PAI / 30 + s * PAI / 1800)); LineTo(hdc, x, y); DeleteObject(hPen); //秒針 hPen = CreatePen(PS_SOLID, 1, RGB(255, 0, 0)); SelectObject(hdc, hPen); MoveToEx(hdc, x0, y0, NULL); x = (int)(x0 + (l - 5) * sin(s * PAI / 30)); y = (int)(y0 - (l - 5) * cos(s * PAI / 30)); LineTo(hdc, x, y); DeleteObject(hPen); return TRUE; }これは、最初の説明を読めばわかりますね。
さて、プログラム起動の初期段階でlpszBufにまだ何も文字列が入っていない状態で この関数が呼ばれます。この時は何もしないで制御を返します。こうしないと 実行時エラーとなります。
GetHMS関数は「xx:xx:xx」のような文字列から時、分、秒の数値を作る関数です。
BOOL GetHMS(char *lpszBuf, int *lpH, int *lpM, int *lpS) { char szTemp[64], *token, szSep[] = ":"; strcpy(szTemp, lpszBuf); token = strtok(szTemp, szSep); *lpH = atoi(token); token = strtok(NULL, szSep); *lpM = atoi(token); token = strtok(NULL, szSep); *lpS = atoi(token); return TRUE; }ここでは、文字列の切り分けにstrtok関数を使っていますが、もっと簡単な方法もあります。 (lpszBufは時刻の10位,lpszBuf+1は1の位です)
なお、strtok関数についてはすでに第78章で解説しているので参照してみてください。
できあがりは左の図のようになります。アナログ部分の色もユーザーがカスタマイズ できるように改良してみてください。
Update 02/Jan/2003 By Y.Kumei