Windows API/文字表示の命令
文字表示の関数
[編集]基本
[編集]「Windows API/Windows デスクトップアプリケーション」で自動作成されたコードをもとに説明しますので、そのコードを用意してください。(空のテンプレートで作ったコードでもウィンドウに文字や画像を描画することは可能。とりあえず本wikibooksでは、説明の単純化のために、先に「Windows デスクトップアプリケーション」で自動作成されたコードで説明する。)
Windows APIの文字表示で、いちばん簡単な関数は、 TextOut
という関数です。
TextOut(hdc, 10, 30, TEXT("テスト"), _tcslen(TEXT("テスト")));
TextOut
は、GDI(Graphics Device Interface)の関数で、指定されたデバイスコンテキストに文字列を描画します。- 第1引数の
hdc
は、描画先のデバイスコンテキストのハンドルを指定します。デバイスコンテキストには、描画の属性(出力先、座標系、使用フォントなど)が設定されています。 - 第2引数の
10
と第3引数の30
は、文字列を描画する Left(X座標)とTop(Y座標)を指定しています。つまり、(10, 30)の座標に文字列が描画されます。 - 第4引数の
TEXT("テスト")
は、描画する文字列を指定しています。TEXT
マクロは、ANSIの場合は通常の文字列、Unicodeの場合はワイド文字列を返します。 - 第5引数の
_tcslen(TEXT("テスト"))
は、描画する文字列の長さ(文字数)を指定しています。_tcslen
関数は、ANSIまたはUnicodeの文字列の長さを自動的に取得できます。
つまり、このコードは「デバイスコンテキスト hdc
に対して、座標(10, 30)の位置に "テスト" という文字列を描画する」ということを意味しています。文字列の長さは_tcslen
で自動取得されているので、ANSIでもUnicodeでも問題ありません。
デバイスコンテキストの設定次第で、この文字列はウィンドウ、ビットマップ、プリンタなど、様々な出力先に描画することができます。
ただし、このTextOut関数など、ウィンドウを描画するための関数を書ける位置は限られていて、
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
という関数(120行目あたりにある。)から始まるブロック中にある条件分岐「 case WM_PAINT: 」のブロック中で書きます。
「// TODO: HDC を使用する描画コードをここに追加してください...」とあるので、 その直後の行で書けばいいのです。
すでに、
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
EndPaint(hWnd, &ps);
}
とあるので、そこに TextOut 関数を挿入し、
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
TextOut(hdc, 10, 30, TEXT("テスト"), _tcslen(TEXT("テスト")));
EndPaint(hWnd, &ps);
}
のようになります。
なお、TextOut関数は改行を表現できません。改行したい場合は、DrawText関数に改行の機能があります。
- TEXTマクロ
Windows API における TEXT()
マクロは、文字列リテラルを UNICODE もしくはマルチバイト文字列として解釈するためのマクロです。これは、プログラムを UNICODE またはマルチバイト文字列のいずれかにコンパイルする際に、文字列リテラルを適切に解釈するために使用されます。
通常、TEXT()
マクロは、プログラムの中で文字列リテラルを使う際に、プリプロセッサ ディレクティブとして使用されます。これにより、コンパイル時のオプションに応じて、文字列リテラルが UNICODE またはマルチバイト文字列として解釈されます。
以下は TEXT()
マクロの例です:
HWND hWnd = CreateWindow(TEXT("MyWindowClass"), TEXT("Hello World"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 300, NULL, NULL, hInstance, NULL);
この例では、CreateWindow
関数に渡される文字列リテラルが TEXT()
マクロで囲まれています。この部分が TEXT()
マクロによって解釈され、コンパイル時の設定に応じて UNICODE またはマルチバイト文字列として扱われます。
このように TEXT()
マクロを使うことで、Windows プログラムは UNICODE とマルチバイト文字列の両方をサポートし、プラットフォーム間の移植性を向上させることができます。
- _tcslen()関数
_tcslen()
は、Windowsプログラミングにおいて文字列の長さを取得するための関数です。
tcは"text character"を意味し、ANSIとUnicodeの両方の文字列に対応しています。
_tcslen()
関数の振る舞いは以下のようになります。
- _UNICODE定義時
wcslen()
関数と同等で、ワイド文字列(Unicode文字列)の文字数を返します。
- _UNICODE未定義時
strlen()
関数と同等で、マルチバイト文字列の文字数を返します。
つまり、_tcslen()
は文字コードに依存せずに文字列の長さを取得できる、ポータブルな関数です。
他にも_tcscat()
、_tcscpy()
、_tcsncmp()
などのtcs系列の関数があり、文字列操作をANSI/Unicodeに依存せずに行うことができます。
使い方は以下のようになります。
TCHAR str[] = TEXT("Hello"); int len = _tcslen(str); // lenには5が入る
このようにtcs系の関数を使うことで、アプリケーションをANSI版とUnicode版で簡単に切り替えられ、ポータブルなコードが書けるというメリットがあります。
- TCHAR型
TCHAR
型は、Windowsプログラミングにおいて使用される文字型です。
具体的には、以下のような特徴があります:
- Unicode対応
- Windowsはユニコード(UTF-16)をネイティブでサポートしており、
TCHAR
型はそのUnicode文字を表現できます。
- Windowsはユニコード(UTF-16)をネイティブでサポートしており、
- ANSI文字列との互換性
- 従来のANSI文字列(マルチバイト文字列)とも互換性があり、同じコードでANSI/Unicodeを処理できます。
- コンパイラスイッチで振る舞いが変わる
_UNICODE
定義時はwchar_t
、そうでないときはchar型
として振る舞います。
- マクロで表現される
TCHAR
型自体はプリプロセッサのマクロで定義されており、本体はchar
型かwchar_t
のいずれかになります。- 例
#ifdef _UNICODE typedef wchar_t TCHAR; #else typedef char TCHAR; #endif
- 文字列リテラルも
_T()
マクロで切り替わるTCHAR
型の文字列リテラルは_T("abc")
のようにマクロで記述します。
TCHAR型を使うことで、ANSIとUnicodeを区別せずに文字列処理ができ、コンパイラスイッチ1つで簡単に切り替えられるのが大きなメリットです。ただし、バッファサイズの違いなどに注意が必要です。Windowsアプリケーション開発では広く用いられています。
- コールバック関数
さて、コールバック関数の冒頭の「WndProc」の部分は我々が変更できる関数名ですが、初期設定をウィザードで自動作成したときに、この名前になります。
また、このコールバック関数をウィンドウと関連づける必要があるため、そのための登録を、冒頭のクラス宣言で行う必要があったのですが、初期設定により既に自動登録されています。
どこで自動登録されているか言うと、60行目あたりを見れば、
// 目的: ウィンドウ クラスを登録します。
とありますが、
そのクラス登録の一連のコード中の70行目あたりに、
wcex.lpfnWndProc = WndProc;
とあるはずです。これにより、コールバック関数 WndProc が、ウィンドウと関連づけられています。
文字表示での変数などの活用
[編集]TextOut(hdc, 10, 20, TEXT("Hello"), 5);
のように、毎回、字数を入力するのは、面倒である。
そこで、「lstrlen()」という、文字列の長さを返す関数を使う。これは、C言語のstrlen関数のWindows版である。
APIプログラミングでは、strlen関数ではなく、lstrlen関数を使わないといけないだろう。
また、毎回メッセージごとにTextOut関数を書き換えるのは非効率であるので、たとえば変数aisatuをあらかじめ用意して、実用的にはTextOut(hdc, 10, 20, aisatu, lstrlen(aisatu));
のように変数(このコードではaisatuが変数)を使うのが効率的である。
コード例
static TCHAR aisatu[60] ;
lstrcpy(aisatu, TEXT("Hello"));
TextOut(hdc, 10, 20, aisatu, lstrlen(aisatu));
のように書くのが実用的である。
TCHARは型であり、C言語でいう文字型 char のWindows版である。このほかにもWindowsにはWinodws版の文字型変数はあるが、とりあえず初心者は、まず TCHAR を知っておけばいい。
C言語では、文字列を宣言するときには、配列として必要な要素数を決めて宣言しなければならないので、とりあえず60とした。
文字列については、ユニコードなど文字コードとの対応の問題があるが、とりあえずTCHARをつかっておけば、あまり問題が起きないだろう。
lstrcpyは、C言語でいう文字列のコピー関数strcpyのWindows版である。
このように、Windows独自の関数がいろいろとあるので、とりあえず初心者は、上記のコードをそのままコピーして、自分の使いたいように改造すれば効率的だろう。
TextOutする事じたいは難しくないのだが、それを実用的なコードにするためにWindows独自の型などを学ばなければならず、そっちが難しい。
TCHAR型とユーザ定義関数
[編集]TCHAR型でもchar型と同様の方法で、下記コードのようにユーザ定義関数の引数に使うことができます。(ただしwindowsAPI側の特殊事情として、HDC hdc などデバイスコンテクストも引数に加えないといけない。)
windowsAPIではあまり実用性の無い例だが、下記コードのようにTCHAR型もユーザ定義関数の引数にすることが可能である。
- コード例
コード上部の「// グローバル変数」宣言部分には、
void testText(HDC hdc, int x, int y, TCHAR* str1) {
TextOut(hdc, x, y, str1, lstrlen(str1));
}
case WM_PAINT: 側は、
// TODO: HDC を使用する描画コードをここに追加してください...
static TCHAR aisatu[60];
lstrcpy(aisatu, TEXT("Hello2"));
//TextOut(hdc, 10, 20, aisatu, lstrlen(aisatu));
testText(hdc, 10, 20, aisatu);
- 実行結果
Hello2
となります。
MSDNのシンタックスの読みかた
[編集]たとえば、マイクロソフトの開発者むけサイト MSDN を見ると、TextOut関数について、
BOOL TextOutA(
HDC hdc,
int x,
int y,
LPCSTR lpString,
int c
);
と、冒頭に、あかたも文法のような何か が書いてある。MSDN TextOutA function
なお、MSDN英語版では、このような冒頭に書かれているアレを「Syntax」と説明している。日本語に直訳すると、「統語論」、「公文法」、「文法」である(※ 参考文献: 『ジーニアス英和辞典 <改訂版> 2色刷』1997年3月1日 改訂版)。
これは、
戻り値の型 関数名 (引数)
が書かれている。
より詳細に説明すれば、
これから説明する関数を使ったときの戻り値の型 その関数名 (引数の順序と型と典型的な引数名)
である。
さて、このシンタックスに書かれている関数は TextOut関数ではなくTextOutA関数だが、これは単に文字コードのANSI版との互換性に対応したバージョンがTextOutA関数なだけである。TextOut関数を使用すれば、Visual Studioが内部でコンパイル時に文字コード関連の設定を適切な設定に置き換えてくれるので、初心者は気にせずTextOutで置き換えてよい。
msdnのサンプルコードでも、TextOut関数で説明している(2019年10月4日に閲覧)。
冒頭の BOOL が気になる人もいるだろうが、冒頭の型は、その関数を呼び出した時の戻り値の型である。BOOL型 という型がある。
TextOut関数にかぎらず、API関数を実際に使う場合、冒頭の型は宣言しない。つまり、
プログラミング中にTextOut関数を実際に呼び出して使用するための関数を記述したい場合は、単に
TextOutA(
HDC hdc,
int x,
int y,
LPCSTR lpString,
int c
);
のような、冒頭のBOOL型を除去した書式で書く必要がある。
つまり、実際のプログラミング作業では、シンタックスにある冒頭の型を除去した書き方で、コードを記述することになる。
もし仮にシンタックスの記法のまま、冒頭の型をAPI関数の呼び出し時に宣言しても、単にコンパイル時にエラーになるだけだろう。
市販の解説書など、書籍によっては、シンタックスのことを「書式」などという用語で、こういう書き方をして説明しているが(※ 参考文献: 林晴彦『明快入門Visual C++ 2010 シニア編』ソフトバンククリエイティブ、2009年8月24日 初版 第1刷発行、24ページ。)、しかし、けっして実際のコードでは、こういうふうにBOOL型などを宣言してから使うわけではない。
またシンタックスでは、数行に分けて書かれているが、これは単に読みやすいようにマイクロソフト社が改行して紹介しているだけであるので、実際の使用では改行せずに1行にまとめて宣言しても良い。
実際、MSDNのサンプルコードでは、
TextOut(hdc,r.left,r.top,text, ARRAYSIZE(text));
のように、冒頭の型を除去したあとに1行でまとめて宣言している。
TextOut関数のほかにもDrawText関数など、さまざまなAPI関数があるが、それらのどれも、実際のコード中での使用宣言の際には、シンタックスの冒頭にある型を除去した形で宣言することになる。
文字列型
[編集]windowsAPIプログラミングの標準的な文字列型は TCHAR 型です。(char型は入門者には、おすすめできません。特殊な用途では char 型も使う。)
TCHR型の宣言は、
TCHAR moji[40] = TEXT("おはよう");
のようになります。
moji[40]
の数値「40」の意味は、「文字列長が英数字の40文字ぶんまで」という意味です(日本語だと20文字ぶんになるかもしれません)。
TCHR 型を用いたサンプルコードなどについては、後述の節を参照してください。
いくつかの例
[編集]変数の値を画面表示
[編集]とにかく整数を表示したい場合
[編集]ウィンドウ内に変数を文字描画できなければ、実用性がありません。
たとえば、タロウくんの去年の身長が約153センチだったとして、今年は約157センチだったとしましょう。(説明を単純化するため、小数部分は切り捨て or 丸めた。)
タロウくんの身長を変数 sintyo としましょう。
さて、TextOut 関数では、変数の値をそのまま表示することは不可能です。
よって、まずC言語の標準ライブラリ関数 sprintf という、これは数値型を文字列型に変換できる標準ライブラリ関数ですが、これのWindows版で _stprintf_s というのがありますので、Win32 API では _stprintf_s を使って、変数を文字列型に変換します。
しかし、この _stprintf_s 関数による文字列への変換は、整数型から文字列への変換しか、うまく変換できません。
浮動小数を文字列に変換すると、誤差が出てきます。たとえば、数値 152.7 を変換した場合、文字列 152.69997 みたいに変換される場合があります。 なので、整数以外の変換は、あまり、オススメできません。
もし、小数点2桁まで文字列に変換したい場合、例えば
- あらかじめ、C言語の関数などを使って、100倍するなどしてから、小数点以下を切り捨てるとか、
あるいは、
- 小数部分と整数部分を別の変数にするなど、
工夫してください。
結局、身長の変数 sintyo を画面に表示するには・・・
int sintyo = 153 ; // この 153 を画面に表示したい
static TCHAR henkan[50] ; // 文字列を格納するための変数 henkan を準備
_stprintf_s( henkan, 200, TEXT("%d"), sintyo); // ここで153を文字列に置き換え、文字列変数 henkan に格納してる
TextOut(hdc, 10, 20, henkan, lstrlen(henkan));
のように、なります。
_stprintf_s の第2引数(上記コードでは200)は、文字バッファーのサイズです。
_stprintf_s で代入先になる第一引数になる文字列変数(例では henkan )は TCHAR 型でなければなりません。(もし char型だと コンパイルでエラーになる。)
_stprintf_s 関数で代入する数値(例では sintyo )は、整数型でなければなりません。もし実数型(たとえば double 型)で数を定義してある場合には、型変換 (int) を使ってから代入します。
- 実数型の場合のコード例
double sintyo = 153 ; // double型の場合
static TCHAR henkan[50] ;
_stprintf_s( henkan, 200, TEXT("%d"), (int) sintyo); // (int)は整数型への型変換である。
TextOut(hdc, 10, 20, henkan, lstrlen(henkan));
となります。
小数を表示したい場合
[編集]例
[編集]とりあえず、値 0.38 くらいの数値の1未満の大きさの入っている変数 hensuu の中身の数値を画面表示する方法を考えましょう。
まず、とりあえず100倍して、それを型変換の (int)
関数で、整数型として型変換したあとに、他の整数型の変数 bufseisuu に入れます。(バッファ用の整数でbufseisuu のつもり)
そのあと、上記の章と同様に sprintf すれば、小数点の上位2ケタの表示がされます。
// 正常に動作するコード例
int hensuu = 0.38 ;
static TCHAR henkan[50] ; // 文字列を格納するための変数 henkan を準備
int bufseisuu = (int) 100 * hensuu ; // ここで型変換している
_stprintf_s(henkan, 200, TEXT("%d"), bufseisuu);
TextOut(hdc, 450, 415, henkan, lstrlen(henkan));
駄目なコード
[編集]C言語の文法上は問題が無くても、しかし次のコードは現実にはVisual Studio のコンパイラが正常動作せず、まちがった実行結果になります。
もし
_stprintf_s(henkan, 200, TEXT("%d"), (int) 100 * hensuu)
(ダメな例)
と _stprintf_s 内部で同時に宣言しても、異常な動作になります。
つまり、
// ダメなコード例
int hensuu = 0.38 ;
static TCHAR henkan[50] ; // 文字列を格納するための変数 henkan を準備
_stprintf_s(henkan, 200, TEXT("%d"), (int) 100 * hensuu ); // ここで異常に読み取りをする
TextOut(hdc, 450, 415, henkan, lstrlen(henkan));
このコードは駄目なのです。
正常動作すれば 0 が入ってるハズなのに、実際には 2034859527 (一例)みたいな、なんかオカシな数が入ってしまいます。
文法上は、このような同時宣言もありそうですが、しかし実際にはウィンドウズVisual Studio のコンパイラ製作の都合などにより、このような同時宣言は異常動作になります。
_stprintf_s 関数にかぎらず、Visual Studio にもとから用意されているライブラリ関数の中で、他のライブラリを使うと、異常動作をする場合が、しばしば、あります。
なので、あまり、使用するライブラリ関数のなかには、別のライブラリ関数を入れないようにしましょう。
その他の方法
[編集]数値を文字列に変換する方法には、_stprintf_s の他にもあります。
_itoa_s や _itot_s などの型変換のライブラリ関数を使う方法です。
しかし、これは、型を厳密に指定する必要があり、そのため、windows独自のさまざまな型や、ポインタ型などについても、プログラマは把握しておく必要があります。
なお、int整数値を TCHAR 形に変換するには、_itot_s を使います。
_itot_s(sintyo , henkan, 200, 10); // 第4引数の「10」は10進数という意味。
TextOut(hdc, 10, 20, henkan, lstrlen(henkan));
のように、なります。
_itot_s の第3引数(上記コードでは200)は、文字バッファーのサイズです。
詳しくは、マイクロソフトの公式サイトを参照してください。 msdn
マイクロソフトではオーバーフローの危険性があるwsprintf関数などは推奨されていなく、strsafe.hの関数を用いることが推奨されています。また、C++で用意された <format> を用いることでこれらを解決できます。
文字の連結
[編集]2つ以上の文字を連結したい場合は、単に、C言語の sprintfs のTCHAR版である _stprintf_s
を使えばいいだけです。
_stprintf_s(mojibuf, TEXT("%s %s"), TEXT("私の名は"), person_name);
TextOut(hdc, 10, 20, mojibuf, lstrlen(mojibuf));
のようになります。 mojibuf をTCHARであらかじめ宣言しておいて、さらにTCHAR型で宣言された person_name にあらかじめ「山田太郎」が代入されていれば、上記のようなコードを実行すれば「私の名前は 山田太郎」と表示されます。
標準C言語(無印C言語)からの変更点は、「 TEXT("%s %s") 」のように、「TEXT( )」 で囲まれるのを忘れないようにしてください。
文字列の多重配列
[編集]TCHAR型の配列は、下記のようになります。
TCHAR syouhinName[3][40] = {
TEXT("牛乳"),
TEXT("オレンジジュース")
};
宣言の配列の見た目が[3][40]
と二重配列なので、ついつい初心者は初期値を代入する際の記述も二次元配列にしがちですが、
しかし最後の [40]
は文字列長を意味するので、syouhinName[3][40]
初期値の書き方は1次元配列のようになります。このような規則は何もwindowsAPIプログラミングに限らず、一般のC言語プログラミングでも同様です(同様に、char型での多重配列の宣言時の最後の数値は文字数を意味する。)
上記例のように、TCHAR 宣言時の配列は2次元配列の書式であっても、TextOut など利用時には次元が一段階だけ下がるので書式は1次元配列でアクセスすることになるので、注意してください(初心者はよく間違えてエラーになり悩む)。