電子工作のためのWindowsプログラミングメモ(仮)
by K.I
Index
- 以前、Macintoshのプログラムをちょっとやったことがあったが、Windowsプログラムは全く作ったことがなかった。
- 今はいろいろな方がWindowsプログラムの解説をされているので、いろいろ参考にさせていただいた。
- 電子工作のための簡単Windowsプログラムということで、自分なりに分かったことを、忘れないうちにメモしておこう。
- 自分なりの解釈で勝手に書いてるので、間違いは沢山あると思う。。
- Macintoshは、QuickDrawというライブラリで、マルチウィンドウシステムを記述している。
- イベント処理で、ウィンドウの書換えを行う仕組みの巧みさに感激した憶えがある。
- Windowsでは、SDKがそれにあたる。
- さらに洗練されたシステムかと思いきや、仕組みはそんなに変らなかった。
- むしろちょっと一貫性が無く、分かりづらいものになっている気がする。
- まぁこれは、最初のQuickDrawがBill Atkinson一人で作られたことによるものが大きいけど、
- Windowシステムの多機能化等で多人数で作られるようになったから、仕方が無いのだろう。
- それに、MacOSは擬似マルチタスクだったから、有る意味単純だったしね。
- まぁ、でももうちょっとシンプルにして欲しかったな〜。
- Windowsのリソースって、いまいちだと思う。
- Macintoshのリソースは、リソースフォークに分かれてたってこともあるけど、簡単に編集出来るResEditってツールもあったし、
- 自分でTMPLリソースを作れば、自前の構造体をテンプレートで編集することも出来た。
- リソース編集用の追加プログラム(これもリソース!)もフリーであったし、簡単なのなら自分でも作れた。
- Windowsって、プロジェクト間でリソースのコピーも出来ないんだもん。(出来るのかもしれないが、単純なCut,Pasteじゃダメ)
- 今はMacintoshもリソースフォークが無くなったから、違うのかなぁ。昔はリソースエディタ弄ってるだけで面白かったな〜。
- ちょっと、愚痴っぽいか。。。
- 普通、Windowsプログラムは高度に抽象化されたMFCライブラリを使って作られるようだ。
- 低レベルルーチンのSDKを使うと、新しい複雑なインターフェースを全て自分で書かなければならなくなるし、保守性も悪い。
- 独自のインターフェースは、使いにくいだけになりかねない。
- 一般的なアプリケーションなら、MFCを使うべきだと思う。
- しかし、趣味の電子工作の場合は、そんなに複雑なインターフェースを使う必要はないし。
- I/OコントロールとかはMFCだけはやりきれないところもあると思う。
- でも一番の理由は、クラスライブラリ使うとイベントループとか動作の仕組みが見えないのがちょっと嫌なのかも。
- 電子工作って自分でいろいろ作れるのが面白いように、プログラムも動作が見えた方が面白い。
- 既存のクラスを継承してサブクラスを作ってというのが、なんか面倒だし。。
- あとで分かったけど、これについては全くの誤解で、 サブクラス化っていうのはsdkでも有効な手法でした。
- 自分の考える、電子工作に必要なプログラムについて考えてみる。
- 電子工作で、Windowsプログラムを作る場合は、メニューとか、ボタンとか、チェックボックスとか基本的なインターフェースだけあれば十分。
- いろんな状態を表示するのに、便利なステータスバーも欲しいかな。
- 基本的な操作パネルは、リソースの作りやすさから、Dialogを使う。
- グラフ等の表示領域は伸び縮みさせたいので、ウィンドウにDialogを貼り付けるようにする。
- でも外部I/Oとのインターフェースについては、 応答が極端に速かったり、遅かったりする。
こんなところかな。。。
[top]
- 自分の場合、Cが一番慣れてるのでCで書きたい。そうなると、
- VC++ →MS VisualC++、まぁ、これが圧倒的に資料が多い。MFCというクラスライブラリがある。
- BCB →Borland C Builder、VCLというクラスライブラリは使いやすいらしい。
- BCC →Borland C Compiler、コンパイラのみフリーで使える。BCC_Developerとか使えば、統合環境的に使える。
- Eclipse →gccを使う環境が幾つかあるようだ。
- 最初に使ったのがVC5だったんだけど、意外と良かったので、VC++が良いかな。
- といっても、VCを使ったのは全部合わせて3ヶ月ぐらいか。。
- Visual Studio 2005 Express Edition は無料で使える。
- また、SDKもちゃんと使うことが出来る。これはMSの ページでも詳しく紹介されている。→結構、太っ腹だぞ!Microsoft!
- これも実際やってみて、ちゃんとSDKのプロジェクトを作ってコンパイルできることを確認した。
- でも大きな問題として、ExpressのVC++だとリソースエディタが使えないんだよね。。
- フリーのリソース編集ツールで作るって手もあると思うんだけど、自分はまだ良く分かってなくてうまく行かなかった。。。
- ということで勿体無いけど、製品版(VS2005 StandardEdition)を買っちゃいました。
- Microsoftの計略に乗っちゃった気がするけどね。
- VC5を使っていたので、すぐ使えると思ったら、全然インターフェースが違う。うーん、失敗したかな?
- 試しに自分ライブラリ(主に文字列操作)をコンパイルしてみる。
- エラーが100個ぐらい出た。。う〜。
- VC5では、確か、プロジェクトメニュー→設定→リンク→一般→ライブラリとかにライブラリの指定があったんだけど、、、無い。。
- 他に方法あるのかな?まぁ出来れば何でも良いけどね。でもこれ、ずいぶん悩んだよ。。。
- strcpyとか、sprintfとか使ってると、古い形式と言われる。
warning C4996: 'strcpy' が古い形式として宣言されました。
まぁ、要するにこの手の関数は、サイズを間違えるとメモリリークの元になるってことなんだろうけど。
- 以下のように直す。自分の場合は、文字列は基本的にstr2551に統一しているので、直すの簡単。
- strcpy(line,"ABC"); → strcpy_s(line,255,"ABC");
- sprintf(line,"%s","ABC"); → sprintf_s(line,255,"%s","ABC");
- sscanf(str,"%lf",&f); → sscanf_s(str,"%lf",&f); →これって何の意味があるんだろうね。
- char文字列の引数で、'LPCWSTR'に変換できませんとのエラーが出る。
- (LPCWSTR)"ABC"と、キャストすればコンパイルは通るけど、文字化けする。なんだこれ?
- 最初は全然分からなかったが、いろいろ調べてみると
- これは、UCS2というUnicodeの一種で文字列を扱っているためらしい。
- 2byteコードに変換しなきゃいけないから、キャストじゃ無理だよね。
- この場合、定数ならL"ABC"というようにすれば良い。
- 文字列が2バイト化してるルーチンは結構ありそうだ。注意が必要だな〜。
変数の場合は
- 変数の場合は、Lを付けるって方法じゃ当然ダメなので、関数にした。
- あんまりスマートじゃないけど、以下のようにWideCharに変換して渡すことにする。
LPWSTR wchar(LPWSTR wline, char *line)
{
MultiByteToWideChar(CP_ACP,0,line,-1,wline,(int)strlen(line)+1);
return(wline);
}
1自分が多用する定義、単に typedef char str255[256]; なんだけど。→255文字あれば、ちょっとした間違いではオーバーし難いし、いつも同じサイズならチェックもやりやすい。簡易メモリリーク防止策になる。
[top]
- テキストの設定については、プロジェクトのプロパティ→General→CharacterSetをUnicodeではなく、マルチバイト指定にすることで従来通りのcharが使えることが解った。
- ここは凄く解りやすい。これ以上の解説はとても出来ないけど、無くなっちゃうと困るので自分なりに理解したことも含めて抜粋してメモしてみた。
- 最初、コンピュータはASCIIコード前提でのプログラムだったので、当然漢字は使えなかった。
- それで、JIS 漢字コードのビットをちょっと弄ることで、1byte 文字列と混在出来るようにしたのが、Shift-JISとかEUCコード。
- 従来の日本語プログラムで使われているこのようなコードを、Multi-byte CharSetと呼ぶらしい。
- それに対して、いろんな国のコードを共通にして1つのコードにしようというのがUniCode CharSet。WindowsではWideCharとも呼ばれるらしい。
- 個人的には思想は理解できるが、UniCodeでは文字数の多い漢字はかなり押し込められてる感じだし、コード体系が増えてメンドクサイ。。。
- 1byte圏の人は、もっと面倒と思ってるんだろうな〜。
- マルチバイトの文字列char*は、WindowsではLPSTRとしても定義されている。
- char* → LPSTR
- const char* → LPCSTR
- それに対して、UniCodeの文字列WCHAR*は、LPWSTRとして定義される。
- WCHAR* → LPWSTR
- const WCHAR* → LPCWSTR
- で、書き分けるのが面倒なので共通に使うための文字列がTCHAR*で、LPTSTRとして定義される。
- TCHAR* → LPTSTR
- const TCHAR* → LPCTSTR
- TCHARは、VC++プロジェクトのCharSetで、charとWCHARを切り替えることが出来る。
- コンパイル時に、指定したCharSetを使ってくれるというわけ。
- 両方の環境に対応したプログラムを記述するためには、TCHARを使った方が良いようだ。
- マルチバイトの文字列の定数(リテラル)の場合は、以下のように定義する。
- Unicodeの場合は、Lを付加することでWCHARとして扱われる。
- const WCHAR* wstr = L"ABC";
- で、共通に記述する場合は、_Tマクロ或いは、TEXTマクロを使う
- const TCHAR* tstr = _T("ABC");
- const TCHAR* tstr = TEXT("ABC");
- VS2005で新規プロジェクトの雛型を作ると、WinMain関数が_tWinMainになっている。
- なんでかな〜と思っていたんだけど、これもTCHAR対応ということらしい。
- これを使うには、tchar.hのインクルードが必要らしい。→雛型のstdafx.hでは、デフォルトでインクルード済み。
- tchar.hを覗いてみると、他にもstrcpyに対して_tcscpyというように、TCHAR用のマクロ定義がされている。
- DLLはコンパイル時にCharSetが決まってしまう。
- さすがに、これは自動的には出来ないということか。
- 自分用の関数なら、ここまでしないと思うけど。公開するなら考えた方が良いのかも。。
- 結局のところ、CharSetをMultiByteにしてコンパイルしてます。
- 古い形式っていうWarningも無視してたりして。だって面倒なんだもん。。
- とりあえず、訳もわからず使っていた型とか関数が、ちょっとだけ解ってきた。。。
- でも、少しづつTCHARを使うように書き直して行った方が良いかもね。
[top]
- オーバーラップするWindowシステムは、非常に単純で巧妙な仕組みで動いている。
- 自分は、初期のMacintoshのWindowシステムしか知らないけど、仕組みは同じようだ。
- Windowシステムは、UpdateイベントとActivateイベントの2つのイベントが分かってれば、OK!2
- 要するに、Window内部の書換えを行うイベント。Windowsでは、WM_PAINTイベントというようだ。
- このイベントが発生したら、Windowの中身を描けばよい。
- ウィンドウを、移動するだけの場合は、描いてあるビットマップをシステムが移動してくれるのでUpdateイベントは発生しない。
- でも、ウィンドウを移動したために、隠れていた別のウィンドウが見えるようになったとする。
- ここで、見えるようになったウィンドウにUpdateイベントが発生する。
- 見えてるものを移動するだけなら、システムは単にデータ転送するだけだから出来るけど、データが無いものについては、表示内容はアプリケーションしか知らないってワケ。
- でも隠れていたのが、ほんの一部だけだった場合は全部書き直すのは勿体ない。というか時間が掛かる。
- そのためUpdateイベントでは、Updateが必要な領域、Updateリージョンが指定される。
- アプリケーションは、Updateイベントが起こった時に、Updateリージョンの部分だけ書き換えれば良いわけだ。
- というか、Updateリージョン部分以外はシステムにより自動的にマスク(クリッピング)されるので、意識する必要はない。
- Updateイベントは、画面の外にはみ出していたWindowを移動して内側に持ってきた時や、2つ重なって背面にあったWindowをクリックして前面に切り替わった時などにも発生する。
- これらのUpdateイベントは、システムが自動的にUpdateリージョンを設定してから発生させるので、アプリケーションはUpdateイベントが来たら中身を描画するだけで良い。
- じゃ、自分でUpdateイベントを発生する必要なんか無いじゃんと思うんだけど、
- 実際、アプリケーション自身もUpdateイベントを積極的に発生させたりする。
- どんな時かというと、
- 例えば、新しいデータを読み込んだので、データが変更された時とか、
- 設定を変えたので、表示を変えなきゃいけない時とか、
- マウスの動きで、線を表示したいって時とか
- それで、本当に書換えが必要な箇所だけUpdateリージョンで指定すれば、描画を速くすることが出来る。
- Windowsでは、InvalidateRectで、Updateリージョンに指定した矩形を追加する。
- InvalidateRgnで、不定形のリージョンを追加することも出来る。
- でも、別にUpdateイベント発生させなくても、直接描いちゃえば良さそうなんだけど。。。
- Windowsシステムの場合、描画するウィンドウは見えてるかどうか分からない。
- 他のウィンドウに隠されていて、見えていないかもしれないし、画面からはみ出してるかも。
- Updateリージョンで、書換えが必要な箇所を明示的に指定することで、効率的なウィンドウ描画を行うことが出来るようになる。
- それに、描画をUpdateイベントだけでしか行わないことにすると、分かりやすいし、デバッグし易くなるという大きな利点がある。
- でも、ただビットマップに線を引くってだけの操作を、Updateイベント一箇所でやろうと思うと、今までの操作を全部憶えとかなきゃならなくなる。
- で、そういう場合は、スクリーン画面上のビットマップと同じ大きさの、メモリ上のビットマップに描画しておく。
- Updateイベントでは、メモリ上のビットマップを画面上へ転送するだけ、というやり方がある。
- 実際の画面に描かないので、Macingothではオフスクリーン・テクニックというけど、Windowでは、ダブルバッファとか言うらしい。
- 直接、画面上に描くと書換えが見えるので、チラチラしてしまう問題があるが、オフスクリーンテクニックを使えば、チラツキを無くすことが出来る。
- オフスクリーンをさらに複数用意して組み合わせることで、さらに複雑な処理をさせたり、いろいろ応用範囲が広いテクニックだ。
- ウィンドウがアクティブになった時に、発生するのがアクティベートイベント。Windowsでは、WM_ACTIVATEイベント。
- 下になっていたウィンドウが、上になった時とか、
- 逆にアクティブじゃなくなった時にも発生する→この場合はディアクティベイトイベント。
- 表示しているものを、アクティブ状態にする。
- ディアクティベイトイベントの場合は、逆にアンアクティブ状態にする。
- 他のウィンドウ、つまり他のアプリケーションが有効になった場合の動作状態に切り替えるなどをすれば良い。
- こちらは、特に何もしなくても良い場合もあるけど、分かってた方が良い。
- 他にも、ウィンドウが生成された時に発生するWM_CREATEとか、ウィンドウのサイズが変った事を示すWM_SIZEイベントとか、いろいろあるんだけど、まぁそれは何となく分かると思うので。
- 実際問題として、この通りに単純には行かない場合もあるとおもうけど。WM_UPDATEとWM_ACTIVATEが基本だし、これをスムーズに動かすようにプログラムの構成を考えるのが、Windowプログラムの面白さじゃないかと思う。
- Dialogとか使えば、コントロールのUpdateやActivateの操作はやってくれるので、かなり楽になる。
2本当かしらん。。間違いもありそうなので、話半分で。。。
[top]
- Windowsプログラムは、基本的には以下のような単純な構造(結構、省略してるけど。)
- 例で示したルーチン名は、VS2005のデフォルトの名前
- ここから、Windowsのプログラムが始まる
- ウィンドウクラスの定義(MyRegisterClass)→メインウィンドウWndProcの指定
- ここで、メインのWndProcの指定や、メニューの指定をする。
- プログラムの初期化(InitInstance)
- イベントループを記述する。
- これがメインプログラム3だけど、最低限、記述するのはこれだけ。
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
MyRegisterClass(hInstance); // ウィンドウクラスの定義
InitInstance (hInstance, nCmdShow) // アプリケーションの初期化
// メイン メッセージ ループ:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg); // これでWndProcへメッセージが送られる
}
}
return (int) msg.wParam;
}
- これは、WinMain内に記述しても良いが、便宜上分けている。
- メインのウィンドウを生成する。
- その他のパラメータ等の初期化
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
g.hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
:
:
}
- イベントループから呼び出される、ウィンドウルーチン。
- 実質的には、これがメインプログラム(メインスレッド)になる。
- 基本的には、このウィンドウに送られるメッセージを実行するだけだ。
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) //メッセージにより処理を行うだけ
{
case WM_CREATE: // メッセージ処理を並べる
:
case WM_COMMAND:
:
}
}
自分用プロジェクトの構成についてのメモ
- VS2005で、VC++用の新規プロジェクトAAAの雛型を作ると、以下のようなファイルが出来る。
- Resource.h →使用しているリソース番号等の定義
- AAA.ico →仮のアプリケーションアイコン
- AAA.rc →リソースファイル
- AAA.h →アプリケーション用のヘッダファイル(最初はresource.hをインクルードしてるだけ)
- AAA.cpp →アプリケーションメイン
- stdafx.h →共通に使うインクルードファイル等を纏めてある
- stdafx.cpp →共通に使うルーチンを入れる(のかな?)
- この中で、使い方を迷うのはstdafx.cppとstdafx.hかな
- 自分で良く使うインクルードファイルや、関数等を定義しておいて、使いまわすってことだと思うんだけど。
- 自分の場合は、良く使うヘッダや定義、自分用関数等をmystd.hに入れてる。
- stdafx.hでインクルードされていないライブラリとか、自分しか使わない型とかを定義してるだけ。
// ---- mystd.h -- my standard setting by K.I
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <setjmp.h>
#include <process.h>
#include <commctrl.h>
#pragma comment(lib, "Comctl32.lib")
#include "chara.h" // 自分用関数ライブラリ
// ---- for string
typedef char str255[256];
:
他にもあるけど省略。
:
- stdafx.hは必要なところにインクルード済なので、mystd.hをstdafx.hにインクルードするだけで準備完了!
- 自分用関数ライブラリにも、必要ならばstdafx.hをインクルードしておく。
- 自分用のライブラリやヘッダファイルは、共通で使うために別ディレクトリに入れておく。
- プロジェクトのプロパティのC/C++コンパイラの設定で、Additional Include Directoriesとして指定。
3関数名に_tが付いてるのは、UniCode対応でコンパイル出来るってことは、後から知った。
[top]
- Windowにはイベントがメッセージとして送られてくる。
- ウィンドウに描画する。
- Invalidateで指定されたUpdate領域のみ描画する。
- ウィンドウがアクティブになった時の処理、また逆にアクティブでなくなった時の処理。
- コマンドの実行
- メニューや、ボタン等から送られたコマンドを実行する。
- ウィンドウのクローズボックスをクリックすると、WM_CLOSE メッセージが送られる。
- DefWindowProc() は WM_CLOSE を受け取ると DestroyWindow() を呼び出す。
- DestroyWindowは、ウィンドウを破棄してWM_DESTROYメッセージを送る。
- これは最後。メッセージループを抜けるために、PostQuitMessage()を呼び出せば良い。
- メッセージはDialogとか使ってれば、システムの方で適当に送られる。
- でも自分でメッセージを送りたい場合には、SendMessageを使う。
LRESULT SendMessage(
HWND hWnd, // Windowのハンドル
UINT Msg, // メッセージ
WPARAM wParam, // Wパラメータ
LPARAM lParam // Lパラメータ
);
- Windowsではメッセージのやり取りは、Windowに対して行う。
- というか、DialogもWindowだし、TextBoxやボタンも全てWindowとして扱われるので、全てWindowハンドルを持つ。
- メッセージはシステムで決められたメッセージ定数があるので、それを指定する。
- それ以外に自分で定義してメッセージを送ることも出来る。その場合、WM_USER以上の数を使う必要がある。
- WPARAM,LPARAMは何だか解らない型だが、実際のところWPARAMはunsigned int、LPARAMはlongだ。
- この使い方は、メッセージによってマチマチで全く統一されていない。
- 面倒だけどメッセージ名で検索すれば、情報が得られるので一応調べた方が良い。
- なんだかんだで、SendMessageはコントロールの設定等、結構使うことが多い。
- コントロール自体もWindowなので、コントロールのWindowハンドルを求めて、メッセージを送る。
- 以下は、UPDOWNコントロールの設定例(MAKELONGは、16bitデータLo+Hiを連結して32bitデータを作るマクロ)
SendMessage(GetDlgItem(hDialog, IDC_CONTROL1), UDM_SETPOS, 0, (LPARAM)MAKELONG(value, 0));
- PostMessageも同様だが、メッセージキューにメッセージを送るので、イベントループで処理される。
- そのためSendMessageのような戻り値を使うことは出来ないが、キューにセットしたら実行終了を待たずに次の処理が出来る。
- それに対して、SendMessageは、メッセージキューに並ばずに割り込んで実行するので、
- すぐ結果が戻ってくるけど、時間が掛かる処理だと並んでいる人に迷惑が掛かる。
- 少なくとも先頭の人が終わるまで、待たなきゃいけないってことかな。
[top]
これはTipsでも何でも無いんだけど、
- グローバル変数を多用すると、プログラムが読みにくくなる。
- とは言っても、プログラムで共通な変数は多いし、使った方が逆にスッキリする場合もある。
- そういう時は、自分の場合はグローバル変数を構造体で一括りにする。
typedef struct {
// ---- handle ----
HWND hMain; // メインウィンドウのハンドル
HINSTANCE hInst; // メインウィンドウのインスタンス
HWND hDlg0, hDlg1, hDlg2, hDlg3; // 子ダイアログのハンドル
long dlg0x, dlg0y, dlg1x, dlg1y; // 子ダイアログの大きさ
HMENU hMenu;
HWND hSbar; // ステータスバー
HBITMAP hBitmap; // オフスクリーンビットマップ
HDC hBuffer; // ダブルバッファ
:
:
} globaldata;
globaldata g;
- こうするとグローバル変数は、g.hBufferのように、g.を付けるだけで簡単だし、グローバル変数ということが分かりやすい。
- 関数に、グローバル変数ごと渡すことも簡単。
- それに現在の環境を保存する場合も、別のglobaldata型の変数に代入するだけ。
- グローバル変数の弊害がほとんど無くなるので、良い方法だと思ってる。
- メニューがあると、標準的なアプリケーションに見えるので。。。
- まずリソースエディタで、メニューを作る。
- ショートカットは、以下のように&を付けて指定する。
ファイル(&F)
- あとは、メインウィンドウクラスの定義(MyRegisterClass)で、
- 実際のメニュー処理は、WM_COMMANDでボタンと同様に行う。
case WM_COMMAND:
switch (LOWORD(wp)) {
case IDM_CHECK:
state = GetMenuState(g.hMenu,IDM_CHECK,MF_BYCOMMAND);
if (state&MF_CHECKED) {
CheckMenuItem(g.hMenu,IDM_CHECK,MF_UNCHECKED|MF_BYCOMMAND);
}
else {
CheckMenuItem(g.hMenu,IDM_CHECK,MF_CHECKED|MF_BYCOMMAND);
}
break;
case IDM_EXIT:
PostMessage(hWnd,WM_CLOSE,wp,lp);
break;
default:
return DefWindowProc(hWnd, message, wp, lp);
}
break;
- メニューハンドルは、HWNDじゃなくてHMENU。
- 状態を表示するのに便利だし、これがあるとアプリケーションっぽいし。。まずは、
- ウィンドウ生成時に、メインウィンドウにステータスバーを生成する
static int sb_size[] = { 100 , 200 , -1 };
case WM_CREATE:
InitCommonControls();
hSbar = CreateStatusWindow(
WS_CHILD | WS_VISIBLE |
CCS_BOTTOM | SBARS_SIZEGRIP ,
(LPCWSTR)"sTaTus" , hWnd , 1
);
SendMessage(hSbar, SB_SETPARTS, 3, (LPARAM)sb_size);
SendMessage(hSbar, SB_SETTEXT, 0 | n, (LPARAM)line);
case WM_SIZE:
SendMessage(g.hSbar, WM_SIZE, wp, lp);
- ステータスバーの分割位置を、ウィンドウのサイズ変更に連動させたい時。
case WM_SIZE:
sb_size[2] = LOWORD(lp); // 右端
sb_size[1] = LOWORD(lp) - 100; // 中央
sb_size[0] = LOWORD(lp) - 200; // 左端
SendMessage(hSbar, SB_SETPARTS, 3, (LPARAM)sb_size);
SendMessage(g.hSbar, WM_SIZE, wp, lp);
- 値を選択して設定できるコンボボックスは良く使うと思う。
- まず、リソースエディタでDialogにコンボボックスを配置しておく。
- コンボボックスのクリック時の展開範囲の指定は、▼をクリックして4指定する。
- 初期化時に、以下のように値を設定しておく。
- この例では、文字列配列listに入れて設定している。
hCombo = GetDlgItem(hwnd,comboid);
for (i=0; i<5 ;i++) {
SendMessage(hCombo,CB_INSERTSTRING,i,(LPARAM)list[i]);
}
SendMessage(hCombo,CB_SETCURSEL,0,0); // 仮に最初の項目を選択
case WM_COMMAND:
switch (LOWORD(wp))
{
case IDC_COMBO:
index = SendMessage(hCombo,CB_GETCURSEL,0,0);
break;
default:
return FALSE;
}
break;
- StaticText等のコントロールに文字列を表示する。
SendMessage(GetDlgItem(hDlg,IDC_COMMAND),WM_SETTEXT,0,(LPARAM)line);
或は SendDlgItemMessage(hDlg,IDC_COMMAND,WM_SETTEXT,0,(LPARAM)line);
ちなみに、コントロールの文字列を取得するには、
SendDlgItemMessage(hDlg,IDC_COMMAND,WM_GETTEXT,sizeof(line),(LPARAM)line);
- ちょっと色を変えるだけなのに、意外と面倒。というか、かなり面倒5。
- オーナードローって、結局全部自分で描けってことだよね。何でこんな面倒な仕様にしたんだろ。
- だから、楽チンプログラムを目指す自分としては、基本的にコントロールの色は変えない。
- とりあえず文字の色を変えるのは比較的簡単なので、それだけにしよう。
case WM_CTLCOLORSTATIC:
// --- コントロールの文字に色を付ける
if( GetDlgItem( g.hDlg0,IDC_SENSOR0_C) == (HWND)lp) {
SetTextColor( (HDC)wp, RGB( 255, 255, 255)) ;
SetBkColor( (HDC)wp, RGB(255,0,0)) ;
return (BOOL)(HBRUSH)GetStockObject(NULL_BRUSH) ;
}
:
:
- フォントはどうでも良いけど、文字の大きさを変えたいことは多い。
- CreateFontでフォントを設定して、コントロールにSendMessageでWM_SETFONTを送れば良い。
static HFONT hFont;
:
case WM_INITDIALOG:
// PANELの初期設定
hFont = SetMyFont("MS ゴシック", 40, 0);
SendMessage(GetDlgItem(hdlg,IDC_STATIC1),WM_SETFONT,
(WPARAM)hFont,(LPARAM)FALSE);
break;
:
- こちらを参考にしました。
- fFontをstaticにしておくのがミソかな。或はグローバルにしてDialogのある限り保持する6。
- hFontは、Dialogが終わる時にDeleteObjectすべきなのかも。
- フォント設定のSetMyFontは、猫でも〜から拝借した以下のルーチンを使っている。
HFONT SetMyFont(LPCTSTR face, int h, int angle)
{
HFONT hFont;
hFont = CreateFont(h, //フォント高さ
0, //文字幅
angle, //テキストの角度
0, //ベースラインとx軸との角度
FW_REGULAR, //フォントの重さ(太さ)
FALSE, //イタリック体
FALSE, //アンダーライン
FALSE, //打ち消し線
SHIFTJIS_CHARSET, //文字セット
OUT_DEFAULT_PRECIS, //出力精度
CLIP_DEFAULT_PRECIS, //クリッピング精度
PROOF_QUALITY, //出力品質
FIXED_PITCH | FF_MODERN, //ピッチとファミリー
face); //書体名
return hFont;
}
- フォントの大きさと、Underline設定の変更用のルーチンを作ってみた。
HFONT setFontSetting(HWND hWnd, int ctrlID, int height, int weight, int underline)
{
LOGFONT logfont;
HFONT hFont;
hFont = (HFONT)SendMessage(GetDlgItem(hWnd, ctrlID), WM_GETFONT, 0, 0);
GetObject(hFont, sizeof(logfont), &logfont);
logfont.lfHeight = height;
logfont.lfWeight = weight;
logfont.lfUnderline = underline;
hFont = CreateFontIndirect(&logfont);
SendDlgItemMessage(hWnd, ctrlID, WM_SETFONT, (WPARAM)hFont, 0);
return hFont;
}
- こんな風にDialogのフォントの属性の設定は出来るけど、色の設定は書き直すしかないのかなぁ。 →サブクラス化の方法
- ちょっとしたアプリケーションならば、メインウィンドウをDialogで作ってしまえばかなり楽になる。
- でもせっかくのWindowsなので、やっぱりメインはWindowにしたい!いろいろ試すと以下のようにするとうまくいった。
- リソースを使わないで、コントロールを配置する場合は、CreateWindowを使う。
- リソースにする場合は、ダイアログのリソースを作成しておき、CreateDialogでウィンドウに追加する。
- とりあえずメインをWindowにして、コントロールはDialogにして貼りつけることにした。
- メインウィンドウ上に描画し、コントロール部分はDialogをメインWindowのリサイズに連動して動かせば良い。
- まず、メインのウィンドウは普通に生成して表示する。
- その際、WS_CLIPCHILDREN指定しておく。
// ---- メインのウィンドウを生成する
hWnd = CreateWindow("xxxMainClass", "xxx",
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
0, 0, 600, 600,
NULL, NULL, hInst, NULL);
if (hWnd == NULL) {
GlobalCleanup();
return FALSE;
}
// ---- メインのウィンドウを表示
ShowWindow( hWnd, nShowCmd ) ;
UpdateWindow( hWnd ) ;
- そして、メインウィンドウのWM_CREATEメッセージで、コントロールを載せたDialogリソースを貼りつける。
- 貼りつけるDialogは、Style=Child指定して作成しておいたものを使う
- Window枠は要らないので、Border=Noneにしておく
case WM_CREATE: //ウィンドウが開いたら
hChild = CreateDialog(hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,(DLGPROC)ChildProc);
ShowWindow(hChild, SW_SHOW);
UpdateWindow(hChild);
- メインウィンドウのWM_SIZEメッセージでは、WindowサイズとDialogを連動させて移動。
- 以下は、Windowの高さをDialogと合わせてから、左側に貼り付ける例。
case WM_SIZE:
GetWindowRect(hWnd, &rt); // Windowサイズ取得
wx = rt.right - rt.left;
wy = rt.bottom - rt.top
GetClientRect(hWnd, &crt); // Windowの枠内のサイス取得
cx = crt.right - crt.left;
cy = crt.bottom - crt.top;
GetClientRect(hChild,&drt); // Dialogリソースのサイズ取得
dx = drt.right - drt.left;
dy = drt.bottom - drt.top;
MoveWindow(hWnd,0,0,wx,dy+(wy-cy),TRUE); //Windowの高さをDialogに合わせる
MoveWindow(hChild, 0, 0, dx, dy, TRUE); //左側にDialogを配置
- Dialogを左側に貼り付けるので、Windowの幅はそのままで、縦方向をDialogに合わせる
- もし、Dialogを上に貼り付けるならば、Windowの高さはそのまま、幅をDialogに合わせれば良い。
- 要は、Windowの大きさと枠内の大きさの差から、枠の幅を計算して、Dialogのサイズに加算するということ。
- メニューがある場合もは、Window枠に含まれるので、この処理でOK
- ステータスバーは、Window枠には含まれないので、GetClientRect等で大きさを求めてWindowサイズ調整時に加算する必要がある
- Dialogを貼り付けるというのは、リサイズ可能なWindowの場合、コントロールを纏めて扱えるので良いと思う
- 例えば、以下のように、Dialog毎にイベント処理を記述する。
BOOL CALLBACK ChildProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_INITDIALOG: // Window の初期化
return TRUE;
case WM_CLOSE: // Window を閉じる
ShowWindow(hWnd, SW_HIDE); // 実際は見えなくしているだけ
UpdateWindow(hWnd);
return TRUE;
case WM_SIZE: // Windowのサイズ変更
return TRUE;
case WM_COMMAND: // Window のコマンド処理
switch (LOWORD(wp)) {
case IDOK:
return TRUE;
case IDCANCEL:
return TRUE;
default:
return (DefWindowProc(hWnd, msg, wp, lp)); //処理しないものはシステムに渡す
}
}
return FALSE;
}
- Aboutダイアログ用のルーチンを用意する
- これは、単に左クリックで抜けるだけの簡単なもの
// Aboutダイアログ表示
LRESULT CALLBACK AboutDlgProc(HWND hdlg, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_INITDIALOG:
g.hAbout = hdlg;
DialogPrintf(hdlg,IDC_VERSION,VERSION);
return FALSE;
case WM_LBUTTONDOWN:
EndDialog(hdlg,IDOK);
break;
default:
return FALSE;
}
return TRUE;
}
- Windowサイズの変更は、WM_SIZEでやってるけど、
- 変更するサイズの制限をその時やると、サイズ変更途中は表示がちらついてしまう。
- サイズ変更途中で制限するために、WM_GETMINMAXINFOイベントを処理する
case WM_GETMINMAXINFO:
MINMAXINFO FAR * lpmm;
lpmm = (MINMAXINFO FAR *)lp;
lpmm->ptMaxPosition.x = 0; // 最大表示時の位置
lpmm->ptMaxPosition.y = 0;
lpmm->ptMaxSize.x = 640; // 最大表示時のサイズ
lpmm->ptMaxSize.y = 480;
lpmm->ptMinTrackSize.x = 100; // 変更可能な最小サイズ
lpmm->ptMinTrackSize.y = 100;
lpmm->ptMaxTrackSize.x = 640; // 変更可能な最大サイズ
lpmm->ptMaxTrackSize.y = 480;
break;
- タイマーで、定期的にWM_TIMERイベントを発生させることが出来る。
- WM_TIMERイベントが発生したら、タイマーIDを確認してからタイマー処理を実行する。
case WM_TIMER:
if (wp != ID_MYTIMER)
return(DefWindowProc(hWnd, msg, wp, lp));
:
タイマー処理を記述
:
この機能は、リアルタイムの処理には向かない。
- ステータスバーの時刻の書き換えとか、定期的だが多少遅くてもかまわない処理に使う。
- アプリケーションの設定内容は、レジストリに保存するのが推奨されているみたいだけど、自分はあまり好きじゃない。
- インストールもアンインストールも面倒だし、基板は簡単に持ち運べるのにアプリケーションが、簡単に持ち運べないのは変だよね。
- それで、設定はiniファイルに保存する。設定をエディタで変更することも出来るし、持ち運びも簡単!
GetCurrentDirectory( 255, dir ); // カレントディレクトリのパスを取得
sprintf(inifile,"%s\\%s",dir,"inifile.ini"); // INIファイルパスを作成
GetPrivateProfileString("SECTION","KEY","DEFAULT",para,255,inifile); // INIファイル読込み
WritePrivateProfileString("SECTION","KEY","PARA",inifile); // INIファイル書き込み
4最初これが分からなかった。分かり辛いよね。
5それに分かり難い。MacのCDEFとかLDEFとかの方がまだ分かり易かったな〜。
6保持が必要ということに関しては自信がない。しなくても良いような気もする。(140606追記)
[top]
- Windowsの描画は、Windowのデバイスコンテキストを得て、そこに描画する。
- Updateイベント(WM_PAINT)の際は、必ずBeginPaintとEndPaintで囲む。
- WM_PAINT以外で描画する場合には、GetDC()でデバイスコンテキストを得て描画し、ReleaseDC() で開放する。
- BeginPaint,EndPaintとの違いは、無効領域の処理をしないというだけ。(だと思う)
- 難しく感じるけど、グラフとか描くんなら、やった方が却って楽。
- 要するに、ウィンドウの表示領域と同じ大きさのバッファをメモリ上に作るだけ。
// ---- オフスクリーンを生成
g.hdc = GetDC(hWnd);
g.hBitmap = CreateCompatibleBitmap(g.hdc , g.bufx , g.bufy);
g.hBuffer = CreateCompatibleDC(g.hdc);
SelectObject(g.hBuffer , g.hBitmap);
SelectObject(g.hBuffer , GetStockObject(NULL_PEN));
PatBlt(g.hBuffer , 0 , 0 , g.bufx , g.bufy , WHITENESS);
ReleaseDC(hWnd , g.hdc);
- 描画する時は、g.hBufferに描けば良い。WM_PAINTは、実際の表示領域に転送するだけ。
g.hdc = BeginPaint(g.hMain, &ps);
BitBlt(g.hdc,0,0,g.bufx,g.bufy,g.hBuffer,0,0,SRCCOPY);
EndPaint(g.hMain, &ps);
- データをグラフ等にプロットすることは良くあるので、データ座標→スクリーン座標を求め方を考えてみる。
- スクリーン座標 → srt(スクリーン左上に原点)
- データ表示領域 → drt(データ左下が原点)
long graph_x(double x) // データ座標xからスクリーン座標を求める
{
double xk;
xk = (srt.right-srt.left)/(drt.right-drt.left);
return((long)(xk*(x-drt.left)+srt.left));
}
long graph_y(double y) // データ座標yからスクリーン座標を求める
{
double yk;
yk = (srt.bottom-srt.top)/(drt.top-drt.bottom);
return((long)(yk*(y-drt.bottom)+srt.top));
}
- 実際は、クリッピングとか考えないと駄目だと思うけど。
[top]
- DLLはダイナミックリンクライブラリで、関数やデータを別プログラムにしておき、実行時にリンクして使用するもの。
- あまり使っていないけど、避けて通れない場合もあるので概要のメモだけ。
- 他の言語から呼出し可能なDLLを記述する場合、__stdcall形式7にする必要がある。
- これでコンパイルすると、DLLとLIBファイルが出来る。
- 呼出し側プログラムでは、DLLImportすれば良い。
- あとは、普通の関数と同じように使うことが出来る。
- これは暗黙的なリンクって奴で、コンパイラがDLLのロードや関数のアドレスを決めるので使うのは楽だけど、
- アプリケーション実行時にDLLが無いと、アプリケーションが動かなくなっちゃうので注意。
- DLLのリンクをコンパイラに任せず、自分でDLLをロードして関数のアドレスを求め、実行するのが明示的リンク。
- これは面倒だけど、DLLが無い場合でもアプリケーションを動作させるようにプログラムすることも出来る。
typedef void(*DLLFUNC)(void); //DLL関数の型を定義
HINSTANCE h_dll; //DLLのインスタンスハンドル
DLLFUNC dll_function; //DLL関数のアドレス
if (NULL == (h_dll=LoadLibrary("dll_function.dll"))) //DLLのロード
DLL読み込み失敗!
dll_function = (DLLFUNC)GetProcAddress(h_dll,"dll_function"); //DLL関数のアドレスを求める
dll_function(); //DLL関数を使う
if (!FreeLibrary(h_dll)) //DLLを開放する
DLL開放失敗!
- ちょっと関数を使うだけで、かなり面倒だ。しかし自分が何をしているか理解しやすい方法とも言える。
- DLLを差し替えて機能追加したいとか、いろいろやりたい時には使える。これはLIBファイルは必要ない。
7何も指定しないと__cdecl形式になるら。これは関数の呼出し時の引数の渡し方の違いらしい。
[top]
- ワンチップマイコンだと割込みが必須なように、それをコントロールするWindowプログラムは、スレッドが必須だと思う。
- スレッドを停止するためには、その関数の最後に以下のように指定するだけ。
_endthread();
- 複数のプログラムが同時に、同じメモリにアクセスするとマズイのは、一般的な割込みと一緒。
- マイコンだと割込み禁止とかにするけど、それと同じように同じ変数をアクセスしないようにすれば良い。
- 単純には、グローバル変数とかをフラグにすれば出来ないことも無いけれど、
- フラグを確認してからセットするまでの一瞬の間に他のプログラムがアクセスすると危ないので注意
- Windowsの同期には、いろんな方法が用意されているのでそれを使うのが簡単。
- ミューテックス →1つだけONになるフラグ
- セマフォ →決まった数だけONになるフラグ
- クリティカルセクション →1つしかアクセス出来ないセクション
- ミューテックスって初めて聞いたけど、何だ? Windowsでは良く使うみたいだけど。
- Muturul Exclusion の略なのか。日本語では排他制御。造語みたいだ。
- ミューテックスや、セマフォはアプリケーション間でも使えるので、用途はいろいろありそう。
- 但し、セマフォの複数のフラグは使いこなすのが難しそうだ。
- とりあえず、以下のように排他制御をマクロにしておいて、切り替えて試してみた。
- グローバル変数によるものは、原理を確認するために作っただけ。一応動くけど、ちゃんと排他されてないので危険。
// ---- Mutual Exclusion ----
#define SYNC_CRITICALSECTION
#ifdef SYNC_MUTEX // Mutexによる排他制御
#define crea_mutex(xxx) {xxx = CreateMutex( 0, FALSE, NULL);}
#define wait_mutex(xxx) {WaitForSingleObject( xxx, INFINITE );}
#define rele_mutex(xxx) {ReleaseMutex( xxx );}
#define clos_mutex(xxx) {CloseHandle(xxx);}
#endif
#ifdef SYNC_CRITICALSECTION // クリティカルセクションによる排他制御
#define crea_mutex(xxx) {InitializeCriticalSection( &xxx );}
#define wait_mutex(xxx) {EnterCriticalSection( &xxx );}
#define rele_mutex(xxx) {LeaveCriticalSection( &xxx );}
#define clos_mutex(xxx) {DeleteCriticalSection( &xxx );}
#endif
#ifdef SYNC_GLOBAL // グローバル変数による排他制御
#define crea_mutex(xxx) {xxx=0;}
#define wait_mutex(xxx) {while(xxx);xxx++;} //(誤動作注意!)
#define rele_mutex(xxx) {xxx=0;}
#define clos_mutex(xxx) {xxx=-1;}
#endif
// ---- zzzアクセス同期用Mutex変数
#ifdef SYNC_MUTEX
HANDLE zzzMutex;
#endif
#ifdef SYNC_CRITICALSECTION
CRITICAL_SECTION zzzMutex;
#endif
#ifdef SYNC_GLOBAL
long zzzMutex;
#endif
- あとはヤバイ変数にアクセスする前にwait_mutexして、アクセスし終わったらrele_mutexすれば良い。
crea_mutex(zzzMutex); // Mutex初期化
:
wait_mutex(zzzMutex); // アクセス前にMutexを取得して、他のアクセスを禁止
// Mutexが使用中なら、開放されるまで待つ
:
ヤバイ変数へのアクセス
:
rele_mutex(zzzMutex); // Mutexを開放する
:
clos_mutex(zzzMutex); // Mutexを破棄する
- 外部I/Oインターフェースの読書きを行う場合、すぐに応答があるかどうか保障されない。
- これで、普通にReadFileすると、待ち続けてしまうことになる。
- スレッドを使えば、他の処理が停止することは無いが、このスレッドを終了出来ないのは同じだ。
- このような場合、非同期I/Oを使うと、入出力の完了を待たずに次の処理を行うことが出来る。
- RS232Cや、USB等の入出力でも必須になると思う。
- 非同期I/Oの手順
- CreateFile時に、6番目のパラメータとしてFILE_FLAG_OVERLAPPEDを渡す。
- ReadFile時に、5番目のパラメータとして、オーバーラップ構造体のアドレスを渡す。
- これにより、ReadFileは読込み完了を待たずに次の処理を実行する。
- 注意点として、
- CreateFileで、OVERLAPPEDフラグを指定した場合は、必ず非同期I/Oを行う必要がある。
- 非同期I/O中は、OVERLAPPED構造体を確実に保持する。
- 非同期I/Oは、いつ処理が終わるかわからない。(当然ですが)
- 非同期I/Oのためには、以下のオーバーラップ構造体を確保する必要がある。
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
- Internal,InternalHighは内部的に使うので、通常気にしなくて良い。
- Offset,OffsetHighは必要に応じて設定する。普通は0で可。
- hEventはイベントフラグを指定する。単純な非同期I/Oの場合はNULLで可。
OVERLAPPED ol;
ZeroMemory( &ol, sizeof(ol) ); // まとめてゼロクリア
ol.Offset = 0; // 必要に応じてセット
ol.OffsetHigh = 0; // 必要に応じてセット
ol.hEvent = NULL; // 必要に応じてセット
- GetOverlappedResult()関数でチェックする。
BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumberOfBytesTransferred, BOOL bWait);
// ---- 非同期での読込み
if (ReadFile(g.hpipe,buffer,sizeof(buffer),&readsize,&g.overlap)) {
// -- 読込み完了
}
else {
if (GetLastError() != ERROR_IO_PENDING) {
// -- エラー
}
}
// ---- 読込み完了を確認
if (GetOverlappedResult(g.hpipe,&g.overlap,&bytes,FALSE)) { //4番目のパラメータをTRUEにすると完了を待つ
// -- 読込み完了
}
else {
if (GetLastError() != ERROR_IO_INCOMPLETE) {
// -- エラー
}
}
- 読込み完了待ちにタイムアウトを設定したいとか、複数の非同期I/Oを行いたい場合は、
- イベントオブジェクトというフラグのようなものを使う。
switch (WaitForSingleObject(g.hEvent,1000)) { // -- タイムアウト1秒に設定
case WAIT_OBJECT_0: // -- 完了
break;
case WAIT_TIMEOUT: // -- タイムアウト
break;
default: // -- エラー
}
| 値 | 意味 |
| WAIT_OBJECT_0 | シグナル状態になった(処理が完了した) |
| WAIT_TIMEOUT | タイムアウトで戻ってきた |
| WAIT_ABANDONED | オブジェクトが別スレッドで廃棄された |
| WAIT_FAILED | 関数が失敗した |
- タイムアウト時間をINFINITEと指定すると、GetOverlappedResult()関数を使用した場合と、大差無い。
- 複数の非同期I/Oを使う場合は、良くわからないので省略。
- イベントオブジェクトは、以下のように直接セット、リセット8することも出来る。
BOOL SetEvent(HANDLE hHandle);
BOOL ResetEvent(HANDLE hHandle);
- 実際には、タイムアウトの時しかイベントオブジェクトを使ったことが無いんだけど。
8自動リセットの設定されたイベントの場合は、ResetEvent出来ない。
[top]
参考にさせて頂いたサイト(感謝)
[top]
[電子工作関連に戻る]
⇒ Disqusの広告がうるさすぎるので基本は非表示にしました