スマホに親しんだ若者がWindows 10で現代的な C++/Win32 のプログラムを作る場合に役立てばいいなと思って作りました。
文字がわからなければ、親か先生に「あいうえお」と「アルファベット」を教えてもらって下さい。
パソコンの使い方が分からなければ、パソコン関係の本を調べる。Windowsを知らなければWindowsの本を調べる。
単語がわからなかったらGoogleで調べる。
- Google https://google.co.jp
漢字がわからなかったら漢和辞典を見る。
無関係なことや細かいことは飛ばしてもよい。
調べても調べてもわからなかったらひとまず、ほっといて先読みしてみる。
最初に高機能なテキストエディタ(サクラエディタ、秀丸、VS Code など)をダウンロード&インストールしないといけない。この本ではサクラエディタの使用を推奨する。
テキストエディタを起動して文字を書いて保存する。保存するには、右上の「ファイル」メニューから「上書き保存」を選ぶ。保存したい場所を選んで「保存」。これでテキストファイルが作成できる。
ファイルを編集したいなら、テキストエディタにファイルアイコンをドラッグ&ドロップしてそれを開き、エディタで編集して保存する。
サクラエディタが嫌いであれば、Visual Studio Code (VS Code) などを使用してもよい。
キーボードの左上の「半角/全角」キーを押すと、日本語入力モードに入ったり出ることができる。日本語入力モードで適当に入力して「変換」キーを押して漢字交じりに変換、Enterキーで確定を繰り返すと日本語が入力できる。
英語のアルファベットでは、ABCDEF...
が大文字で、abcdef...
が小文字である。
以下の2つの文字列を比較してほしい:
ABCDEFGHIJKLMN
ABCDEFGHIJKLMN
前者が全角(ぜんかく)文字で、後者が半角(はんかく)文字である。昔のパソコンでは全角文字は半角文字の二倍の幅を持っていた。半角文字の多くは英語のASCII文字と互換性があり、英語圏でも同じデータでやり取りできるが、全角文字を日本語ではないパソコンで表示しようとすると問題が発生することがある。
プログラミングでは主に半角文字を使う。
プログラミングで使うファイルや識別子の名前は、基本的に日本語や全角文字は使えないと考えた方がいいだろう。ファイル名には使えない特殊な文字がいくつかある。
ファイル名は1字でも間違うと動かないことがあるので、全角半角の区別、大文字小文字の区別ができるようになろう。
Windowsではファイル名には大文字小文字の区別がないが、他のシステムでも使うファイルについては大文字小文字を意識する必要がある。
ファイル名の最後に「.txt」「.png」などのドット(dot)で始まる文字列が付いているのが拡張子。拡張子は、ファイルの種類を表している。
拡張子は半角でなければならない。Windowsでは拡張子がファイルの種類を区別している。拡張子が1字でも違うとファイルが開かないことがある。
拡張子が見えなければ、Windowsで拡張子が見える設定にしないといけない。開発現場では拡張子表示は必須。
フォルダとは、ファイルを入れる入れ物であり、エクスプローラやデスクトップを右クリックして出てくる「新規作成」メニューの「フォルダー」を選べば作成できる。さらにフォルダの中に別のフォルダや複数のファイルを入れることができる。ファイルをまとめたり、分類するときに便利だ。
例えば、Windows 10 には、C:
ドライブにWindows
というフォルダ(C:\Windows
)があり、この中にWindowsのシステムファイルが含まれている。さらにこの中にsystem32
フォルダ(C:\Windows\system32
)がある。
このC:\Windows
やC:\Windows\system32
というのが、フォルダのパス(path)と呼ばれる文字列で、フォルダの位置を表す。passwordとまぎらわしいので、混同しないように。\
(バックスラッシュ)は、パスの区切りと呼ばれる。パスの区切りは環境によって異なり、Windowsでは\
であり、Bashでは/
(スラッシュ)である。
\
はバックスラッシュ(backslash)という記号であり、/
はスラッシュ(slash)という記号である。日本語キーボードでは半角モードで「ろ」や「め」を押すと入力できる。日本語環境では、バックスラッシュは半角の円記号(¥)で表示されることもある。
パスは、相対的な位置を表す「相対パス」と、絶対的な位置を表す「絶対パス」に分類される。
1個のドット(.
)は、現在のフォルダを表す特殊なフォルダ名で、2個のドット(..
)は、一つ上のフォルダを表す特殊なフォルダ名である。例えば、C:\Windows\system32\..
はC:\Windows\system32
の一つ上、すなわち、C:\Windows
と同じ意味である。
C:\Windows\system32
は絶対パスで、..\system32
は相対パスである。
テキストデータの中から素早く文字列を探すには、検索機能を使う。サクラエディタでは、Ctrl+F
キー(Ctrl
キーを押しながらF
キー)で検索を開始できる。
複数のテキストファイルからある文字列を探すには、サクラエディタでgrep
(グレップ)機能を使う(「検索」メニュー→「Grep...」)。
Windowsでは、アプリでファイルを開くと、アプリはそのファイルが変更されないようにロックすることができる。ロックされたファイルは開いたり、変更できない。ファイルが開けない場合は、すでに開いているプログラムがないか確認しよう。
Windowsのプログラムのほとんどは、.exe
という拡張子を持ったEXEファイルで出来ている。EXEファイルをダブルクリックするとプログラムを起動できるかもしれない。.exe
の実行に必要なDLL
ファイル(拡張子.dll
)が足りないと、実行に失敗する。DLLとはdynamic linked libraryの略で、EXEの機能を拡張するために、実行の際に読み込まれる外部ライブラリのことである。ライブラリ(library)というのは、C言語などで使用できる関数をまとめたものである。
Windowsで実行したプログラムは、「プロセス」という単位で実行される。プロセスはタスクバーを右クリックすれば出てくる「タスクマネージャ」から操作できる。1個のEXEファイルから複数のプロセスを同時に起動できる。
プロセスは、使用しているEXEファイルとDLLファイルをロックする。そのため、実行中はEXEファイルを変更できないので、ビルドの前にそのプログラムを終了させる必要がある。終了できない場合はタスクマネージャからプロセスを強制終了しよう。
パソコン(PC)の OS は64ビット版のWindows 10で決まり。記憶媒体には、ハードディスク(HDD)は遅いので SSD の 200GB 以上を選択する。メインメモリは4GB以上を選択する。
ノートパソコンとデスクトップパソコンの2種類あるが、デスクトップの方が安い。オフィスソフトは必要になったときに買えばいい。LibreOfficeという無料のオフィス互換ソフトもある。これで全部でだいたい6万円くらいになる。中古を選べば5万円くらいで購入できるが、品質が保証されないので注意。
開発を進めるとき、「リファレンス マニュアル」というものがあれば、関数の使い方をすぐ確認できて便利だし、時間の節約になる。C/C++のマニュアルや「Win32 Programmer's Reference」などをパソコンに入れて使うとよい。
通常、WindowsでC/C++コンパイラを使いたい場合、Visual Studio か MSYS2 を使うといい。Visual Studio(以下VS)はパワフルな統合開発環境(IDE)で非常に使いやすい。MSYS2はCUIベースの開発環境で、低スペックのPCでも使える。MSYS2は最小限のWindows開発環境「MinGW」にLinuxライクなBashシェルを追加したようなもので、ちょびっとLinux風に使える開発環境だ。
簡単なプログラムをコンパイルしたい場合は、最近ではオンラインコンパイラという選択肢もあるが、ソースファイルが1つしか使えないとか、そもそもWin32をサポートしていないなどの制限がある。
この文書では簡単のため、開発環境として ReactOS Build Environment (RosBE) を使用する。RosBEなら
- 標準でC/C++/Win32コンパイラがある
- ビルドしたアプリはそのままWindows XPで実行できる
- ビルド支援のCMake/Ninjaがついてくる
という利点がある。
では早速、Windows版のReactOS Build Environment (RosBE) をインストールしてみよう。次のリンクからWindows版のRosBEを選択すれば、ダウンロードできる。
ダウンロードに成功したらインストールしよう。ファイルアイコンをダブルクリックすれば、インストールが始まる。
インストールに成功したら、デスクトップにRosBEのアイコンが出来ているはずである。ダブルクリックしてRosBEを起動しよう。
起動すると次のようなRosBEのコマンドプロンプトが表示される。
著者のユーザ名はkatahiromz
なので、読者の環境ではいくつか表示が異なるかもしれない。
コマンドプロンプトは、コマンドを受けとって、何かの処理を行う対話型端末である。1コマンドを入力し、Enterキーを押すと、コマンドプロンプトでコマンドを実行できる。
以下では、コマンドプロンプトの使い方を少し解説する。
CD
コマンドはディレクトリ(フォルダの位置)を移動するコマンドである。
cd ..
で一つ上のフォルダに移動できる。cd (フォルダパス)
で現在のディレクトリを移動できる。相対パスと絶対パスのいずれかを指定できる。
試しに、マイドキュメントに移動してみよう。マイドキュメントの位置(例えば、筆者の環境ではC:\User\katahiromz\Documents
)を確認して、CDコマンドで指定すると移動できる。
DIR
コマンドで現在のフォルダにあるファイルやフォルダの一覧を見ることができる。CD
コマンドでC:\Windows\system32
に移動してdir
を実行してみよう。
C:\Windows\system32
には、大量のファイルがあるので、ここでDIR
コマンドを実行すると、実行が止まらなくなるかもしれない。Ctrl+C
を押すと、現在実行中のコマンドを中断することができる。
DEL
コマンドでファイルを削除できる。マイドキュメントに移動して、DEL "(重要なファイルの名前)"
を実行して、重要なファイルを消してみよう。貴重な記録を消すことができる。
MD
コマンド(MKDIR
コマンド)でフォルダを作成できる。
C:
ドライブのルートに「dev
」というフォルダを作り、その中に「cxx
」というフォルダを作ってみよう。
cd C:\
md dev
cd dev
md cxx
dir
このようにコマンドを入力すると以下のような表示になる。
start .
(スタート、スペース、ドット)コマンドを使えば現在のフォルダをエクスプローラーで開くことができる。
C/C++では、ソースファイル(拡張子.c
/.cpp
)をC++コンパイラでコンパイルしてできたオブジェクトファイル(拡張子.o
/.obj
)をライブラリ(lib*.a
/*.lib
)とリンクすると、EXEファイルやDLLファイルができる。コンパイルとリンクを合わせてEXEファイルやDLLファイルなどを作ることを構築(ビルド; build)という。ビルドのイメージは次のようになる。
それでは、実際にソースファイルを作成しよう。C:\dev\cxx
に次のような内容のhello.cpp
というテキストファイルを作成して下さい。
#include <cstdio>
int main(void)
{
printf("Hello, world\n");
}
念のため、プログラミングで使う記号の打ち方と名前について確認しよう。以下は半角英数モードで入力する。
#include
の#
はシャープ記号で、キーボードのShift
キーを押しながらキーボード左上の「3
」を押せば入力できる。<cstdio>
の<
と>
は、不等号であり、Shift
キーを押しながらキーボードの「ね
」、もしくは「る
」を押せば入力できる。(
と)
は、丸カッコであり、Shift
キーを押しながらキーボード中央の「8
」または「9
」を押せば入力できる。"
は、二重引用符であり、Shift
キーを押しながらキーボード左上の「2
」を押せば入力できる。\
は、バックスラッシュ(\)または半角の円記号(¥)であり、半角英数モードでキーボード右下の「ろ
」を押せば入力できる。;
は、セミコロンである。半角英数モードでキーボードの「れ
」を押せば入力できる。
printf
の呼び出しが右にずれているのは、インデント(indent; 字下げ)といって、プログラムの構造を見やすくするためである。キーボードの左側のTab
キーを押せばインデントできる。
RosBEではC/C++コンパイラとしてgcc
/g++
を使用する。CD
コマンドでC:\dev\cxx
に移動し、次のように入力すればhello.cpp
をコンパイル・実行できる。
g++ hello.cpp -o hello
hello
エラーメッセージが表示されたら、何か文字を間違えているのでしょう。hello.cpp
の内容を注意深く確認して下さい。
無事にコンパイルが終了すると、EXEファイルC:\dev\cxx\hello.exe
が作成され、hello.exe
が実行可能になる。hello
と入力すると、同じフォルダにあるhello.exe
が実行され、Hello, world
と表示される。
このようにしてRosBEで作成したプログラムは、おそらくWindows XPでも実行可能である。
これで、あなたはWindows XP以降で動作するC++プログラムを作成できた。
次はWin32プログラム(アプリ)を作ってみよう。次のような内容のhello2.cpp
を作って下さい。
#include <windows.h>
#include <cstdio>
int main(void)
{
if (MessageBoxA(NULL, "Yes or No?", "Test",
MB_ICONINFORMATION | MB_YESNO) == IDYES)
{
printf("You chose YES\n");
}
else
{
printf("You chose NO\n");
}
}
g++ hello2.cpp -o hello2
hello2
これを実行すると、黒い画面の上にメッセージボックスが表示され「はい」か「いいえ」の選択を促される。「はい」を選択すると"You chose YES"
と表示されてアプリが終了する。「いいえ」を選択すると"You chose NO"
と表示される。MessageBoxA
はメッセージボックスを表示するWin32 API関数MessageBox
のANSI版であり、これを使うために事前に#include <windows.h>
が必要になる。
コマンドプロンプトからhello2.exe
を実行できるし、hello2.exe
をダブルクリックしても実行可能である。
これであなたもWin32プログラムを作り、Win32 API関数を呼び出すことでWin32 APIの入口に入ったのである。「見習いWin32プログラマ」の称号を授けよう。
ここまで、コマンドプロンプトでいちいちコマンドを入力してビルドをしていたが、複雑なプロジェクトになると、コマンド入力では対応できない。C/C++ではCMakeというビルド支援ソフトを使うと、ビルド処理を自動化できる。
CMakeでは次のような手順でプロジェクトをビルドする。
- プロジェクトのフォルダに
CMakeLists.txt
というテキストファイルを作成する。CMakeLists.txt
には、プロジェクトのビルド方法をCMakeの言葉で記述する。 - CMakeプログラムを起動して
CMakeLists.txt
からビルドに必要なファイルを生成させる。 - 生成されたファイルを使ってプロジェクトをビルドする。
RosBEでは実際にビルドを実行させるのはNinjaという名のジェネレータ(generator)である。MinGWやMSYS2ではMakefile であり、Visual Studioではプロジェクトファイル&ソリューションファイルである。cmake
の-G
オプションでジェネレータを指定できる。ジェネレータの一覧はcmake -G
コマンドで見ることができる。
それではCMake/Ninjaによるビルドを試してみよう。まずは、プロジェクトに必要なファイルをフォルダで分けないといけない。C:\dev\cxx
にhello2
というフォルダを作成し、そこに先ほど作成したhello2.cpp
を移動する。そして、C:\dev\cxx\hello2
フォルダに次のようなファイルCMakeLists.txt
を作成する。
cmake_minimum_required(VERSION 2.4)
project(hello2 C CXX RC)
add_executable(hello2 hello2.cpp)
一行目のcmake_minimum_required
は、CMakeの最小バージョンを指定するものである。次のproject
はプロジェクト名と使用する言語(C CXX RC
)を指定するものである。最後の行のadd_executable(hello2 hello2.cpp)
はhello2.exe
というEXEファイルのビルド方法を指定するものである。
RosBEでCD
コマンドでC:\dev\cxx\hello2
フォルダに移動し、次のようにコマンドを実行すると、hello2
のビルドが完了する。
cmake -G "Ninja" .
ninja
実行結果は次の通り。
ビルドの際にEXE以外にさまざまなファイルが作成されるが、無視してもよい。
次は、ダイアログ ボックス(dialog box; 通称ダイアログ)を表示するプログラムを作成してみよう。ダイアログとはユーザーに対して対話的なウィンドウのことであり、メッセージボックスもダイアログの一種である。
しかし、簡単にダイアログを作成するには「リソースファイル」(拡張子.rc
)というものが必要になる。そこで、リソースを編集・作成するための「リソースエディタ」というものが必要になる。
この文書ではリソースエディタとして「リソーエディタ」(RisohEditor)を使用する。次のリンクからリソーエディタをダウンロード&インストールして下さい。
リソーエディタをインストールすれば、次のようなアイコンがデスクトップに作成される。
リソーエディタの詳しい使い方については、次のまとめサイトを参照されたい。
ではダイアログアプリを作成しよう。C:\dev\cxx
にdialog
というフォルダを作成し、次のC++ソースファイルdialog.cpp
を作成する。
#include <windows.h>
#include <windowsx.h>
#include <strsafe.h>
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
return TRUE;
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
return 0;
}
急にプログラムがややこしくなったが、1つ1つ理解していけば問題ない。
#include <windows.h>
は、Win32 APIを使うために必要なヘッダをインクルードする。#include <windowsx.h>
は、HANDLE_MSG
マクロを使用するために必要である。HANDLE_MSG
マクロは、メッセージハンドラとウィンドウプロシージャ(もしくはダイアログプロシージャ)を結び付けるのに使う。メッセージハンドラとは、ウィンドウで発生したイベントに応じて発生するメッセージを処理する関数である。ダイアログプロシージャは、ここではDialogProc
関数のことである。ダイアログプロシージャは典型的なイベント駆動型プログラミングを実装する。WM_INITDIALOG
メッセージはダイアログの初期化のときに発生する。WM_COMMAND
メッセージはダイアログでコマンドが発生したとき(ボタンが押されたときなど)に発生する。
#include <strsafe.h>
については後述する。
ここでは、C言語で慣れ親しんだmain
関数の代わりにWinMain
という関数を使う。main
関数を使うと黒い画面が表示されるが、ウィンドウアプリでは黒い画面は不要なのでmain
関数は使わない。DialogBox
関数はダイアログを表示するAPI関数である。HWND
は、ウィンドウのハンドルを格納する型である。UINT
はunsigned int
型と同じである。WPARAM
やLPARAM
は、ポインタと同じサイズの整数型である。DialogProc
関数ではWM_INITDIALOG
メッセージとWM_COMMAND
メッセージを処理している。DialogBox
やWM_INITDIALOG
などの意味については、それをインターネットで検索すれば出てくる。MAKEINTRESOURCE
マクロは、整数のリソース名を指定するのに使う。
開発が進むにつれて、複雑なコードを何度も入力するはめになるが、WinMain
やDialogProc
、OnInitDialog
などのよく使うコードは、コピーしたり、テンプレートを使ったり、マクロなどで自動入力すれば問題ない。入力補助としてMsgCrack
というソフトがあるので活用されたい。
次はリソースファイルである。リソーエディタで以下の手順に従ってdialog_res.rc
ファイルを作成する。dialog_res
の下線(_
)は、Shift
キーを押しながら「ろ
」で入力する。名前に_res
を付けたのはオブジェクトファイルの名前を衝突させないためである。
- リソーエディタを開く。
- 「編集」メニューから「追加」→「ダイアログを追加」を選び、「リソースの名前」に「1」(いち)を入力し、「OK」ボタンを押す。
RT_DIALOG
→1
→日本語
が追加される。 - 「ファイル」メニューから「名前を付けて保存」を選び、
c:\dev\cxx\dialog
に「dialog_res.rc
」という名前で保存する。 - 「保存」オプションが表示されたら、「言語別にファイルを分ける」のチェックを外し、「OK」ボタンを押す。
- 左下に「ファイルを保存しました」と表示されたら完了。リソーエディタを閉じる。
このように保存すると、dialog_res.rc
ファイルは次のような内容になる。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
...(以下略)...
このようにリソースを使えば、日本語の埋め込みも問題ない。
次に次のような内容のCMakeLists.txt
を作成する。
cmake_minimum_required(VERSION 2.4)
project(dialog C CXX RC)
add_executable(dialog WIN32 dialog.cpp dialog_res.rc)
target_link_libraries(dialog PRIVATE comctl32)
さらに、cmake -G "Ninja" .
とninja
を実行すれば、dialog.exe
がビルドされる。
一行ずつ解説しよう。CMakeLists.txt
のadd_executable
にWIN32
があるのは、main
関数を使わず、WinMain
関数を使うためである。WinMain
関数を使えば起動時に黒い画面は表示されない。
リソースをコンパイルするために、dialog_res.rc
を追加した。
target_link_libraries
についてはdialog.exe
にリンクするDLLファイルcomctl32
を指定している。このcomctl32
については後述する。
よく使うDLLファイルのkernel32
とuser32
については明示的にリンクしなくても勝手にリンクされる。
それではdialog.exe
を実行してみよう。次のような何の変哲もないダイアログが開かれるはずである。
「OK」や「キャンセル」を押したらEndDialog
関数でダイアログを終了するだけだ。EndDialog
がなければ終了しないダイアログアプリになる。
EndDialog
の呼び出しの前に処理を追加すれば、ボタンを押したときに何か処理を行うことができる。例えば、「OK」ボタンを押したときに、test.txt
というテキストファイルを作成するプログラムに改造してみよう。WM_COMMAND
メッセージのプロシージャのOnCommand
関数を次のように改造し、OnOK
関数を追加する。
void OnOK(HWND hwnd)
{
if (FILE *fp = fopen("test.txt", "w"))
{
fprint(fp, "This is a test.\n");
fclose(fp);
}
EndDialog(hwnd, IDOK);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
これで「OK」ボタンを押すと、"test.txt"
というファイルを作成するようになった。
このダイアログアプリにメインアイコンを追加しよう。アイコンを追加すれば、アプリのアイコンを変更することができる。
- インターネットから「無料素材」のアイコンファイル (拡張子
.ico
)を探してダウンロードする。 - リソーエディタで
dialog_res.rc
を開く。 - 「編集」メニューから「追加」→「アイコンを追加」を順番に選ぶ。
- 「参照」ボタンを押してアイコンファイルを指定する。
- リソースの名前に「1」(いち)と入力する
- 「OK」ボタンを押すと、
RT_GROUP_ICON
とRT_ICON
が追加される。 - 上書き保存する。
さらにダイアログでこのアイコンを使うようにWM_INITDIALOG
のプロシージャを変更しよう。
static HICON s_hIcon = NULL;
static HICON s_hIconSmall = NULL;
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HINSTANCE hinst = GetModuleHandle(NULL);
s_hIcon = LoadIcon(hinst, MAKEINTRESOURCE(1));
s_hIconSmall =
(HICON)LoadImage(hinst, MAKEINTRESOURCE(1), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), 0);
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)s_hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)s_hIconSmall);
return TRUE;
}
一つずつ説明しよう。s_hIcon
とs_hIconSmall
はアイコンのハンドルを格納するための変数である。ハンドルというものを使えば、Win32の様々な操作対象を操作できる。GetModuleHandle(NULL)
は現在のアプリのインスタンス(モジュール)を取得する。これは現在のEXEのアイコンを読み込むために使用する。LoadIcon
は通常の大きさ(32x32)のアイコンを読み込むAPI関数だ。リソース名には1
を指定している。LoadImage
API 関数は小さいアイコン(16x16)を読み込むために使っている。GetSystemMetrics
API 関数は小さいアイコンのサイズを取得するために使用する。小さいアイコンは通常16x16ピクセルだが、システムによっては違う値の可能性もある。
SendMessage
関数でダイアログのウィンドウにWM_SETICON
メッセージを送信すると、ダイアログにアイコンをセットできる。
使い終わったアイコンは、DestroyIcon
関数で破棄した方がよい。WinMain
関数にアイコンの破棄コードを追記する。
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
では、再びninja
を実行してビルドしよう。EXEファイルのアイコンが変更され、実行するとアイコン付きのダイアログになる。
ここまでのソースdialog.cpp
は以下の通り。
#include <windows.h>
#include <windowsx.h>
#include <strsafe.h>
static HICON s_hIcon = NULL;
static HICON s_hIconSmall = NULL;
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HINSTANCE hinst = GetModuleHandle(NULL);
s_hIcon = LoadIcon(hinst, MAKEINTRESOURCE(1));
s_hIconSmall =
(HICON)LoadImage(hinst, MAKEINTRESOURCE(1), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), 0);
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)s_hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)s_hIconSmall);
return TRUE;
}
void OnOK(HWND hwnd)
{
if (FILE *fp = fopen("test.txt", "w"))
{
fprint(fp, "This is a test.\n");
fclose(fp);
}
EndDialog(hwnd, IDOK);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
リソースファイルdialog_res.rc
は次のようになる。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
...(以下略)...
次にダイアログにラベルとテキストボックスを追加・配置する。
リソーエディタでdialog_res.rc
を開く。RT_DIALOG
→1
→日本語
を選択し、ダブルクリックする。「ダイアログの編集」が開かれる。
ラベルを追加する。右クリックして「コントロールの追加」を選ぶ。
「定義済みControl:」にLTEXT
と入力する。「キャプション:」に「整数:」と入力する。「ID:」に「stc1
」と入力する。「OK」ボタンを押す。
ラベル「整数:」が追加されるのでサイズと位置を調整する。
次にテキストボックスを追加する。右クリックして「コントロールの追加」を選ぶ。
「定義済みControl:」に「EDITTEXT
」と入力し、「ID:」に「edt1
」と入力する。「OK」ボタンを押す。
ボタンが追加される。位置とサイズを調整する。
「ダイアログの編集」を閉じ、変更内容を上書き保存する。
これでラベル(LTEXT
)とテキストボックス(EDITTEXT
)を追加できた。ninja
を再び実行してビルドしよう。
ビルドに成功したら、dialog.exe
を実行してみよう。
2個のコントロールが追加されている。リソースファイルdialog_res.rc
は次のようになる。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
LTEXT "整数:", stc1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
...(以下略)...
これでコントロールIDがstc1
のSTATICコントロール(ラベル)と、コントロールIDがedt1
のEDITコントロール(テキストボックス)が追加された。
入力された整数の2倍の整数を求めるという処理を追加してみよう。現在、OnOK
関数は次のようになっている。
void OnOK(HWND hwnd)
{
if (FILE *fp = fopen("test.txt", "w"))
{
fprint(fp, "This is a test.\n");
fclose(fp);
}
EndDialog(hwnd, IDOK);
}
これを書き換えて2倍の整数を求めるようにする。
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE) * 2;
WCHAR szText[64];
StringCbPrintfW(szText, sizeof(szText), L"%d", n);
MessageBoxW(hwnd, szText, L"Nibai", MB_ICONINFORMATION);
EndDialog(hwnd, IDOK);
}
テキストボックスに入力された整数を取得するには、GetDlgItemInt
というAPI 関数が用意されているのでこれを使う。
StringCbPrintfW
は<strsafe.h>
で宣言されていて、C言語のsprintf
に似た関数だが、Unicodeに対応しているところとバッファサイズを指定できるところが違う。
バッファサイズの指定により、バッファオーバーフローを回避できる。
StringCbPrintfW
のCb
とは、count bytes
の略でバイト数を意味する。第二引数にsizeof(szText)
を指定するのでCb
である。
C言語の文字列リテラルの最初にL
が付いていると、Unicode文字列になる。
よってL"%d"
は、Unicode文字列リテラルである。MessageBoxW
関数はメッセージボックスを表示してボタンが押されるまで待つ関数MessageBox
のUnicode版である。
ではninja
を実行して再びビルドしてdialog.exe
を実行しよう。
テキストボックスに「1111」と入力して「OK」ボタンをクリックすればメッセージボックスで「2222」と返ってくる。その後、メッセージボックスを閉じると、ダイアログは自動的に閉じられる。
ここまでのソースを以下に示す。
#include <windows.h>
#include <windowsx.h>
#include <strsafe.h>
static HICON s_hIcon = NULL;
static HICON s_hIconSmall = NULL;
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HINSTANCE hinst = GetModuleHandle(NULL);
s_hIcon = LoadIcon(hinst, MAKEINTRESOURCE(1));
s_hIconSmall =
(HICON)LoadImage(hinst, MAKEINTRESOURCE(1), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), 0);
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)s_hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)s_hIconSmall);
return TRUE;
}
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE) * 2;
WCHAR szText[64];
StringCbPrintfW(szText, sizeof(szText), L"%d", n);
MessageBoxW(hwnd, szText, L"Nibai", MB_ICONINFORMATION);
EndDialog(hwnd, IDOK);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
リソースファイルdialog_res.rc
は以下の通りである。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
LTEXT "整数:", stc1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
...(以下略)...
ダイアログを開いてTab
キーを何度か押してみよう。
初めの状態。
1回Tab
キーを押す。
2回目。
見ればわかるように、Tab
キーは「キーボード フォーカス」というものを移動させる。フォーカスというのは、現在のキーボード操作対象のコントロールのことである。最初は「OK」ボタンにフォーカスがある。次は「キャンセル」ボタンにフォーカスが移る。最後にテキストボックスにフォーカスが移る。もう一度Tab
キーを押すと「OK」ボタンに戻る。
テキストボックスにはフォーカスがないと入力できない。最初にテキストボックスにフォーカスがある方がユーザーにとって親切だろう。そこで、dialog_res.rc
をテキストエディタで開いてコントロールの順序を次のように変えて、上書き保存する。
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
LTEXT "整数:", stc1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
これでWM_INITDIALOG
メッセージでTRUE
を返すと、自動的にedt1にフォーカスが当たるようになる。ninja
を実行してもう一度試してみよう。
今度は、最初にedt1
にフォーカスが当たる。これでダイアログを開いたらすぐに整数を入力できる。
ダイアログを開いているときは、キーボードでEnter
を押すと、デフォルトのボタン(DEFPUSHBUTTON
)が押されるようになっている。キーボードの左上のEsc
キーは「キャンセル」ボタン(IDCANCEL
)と同じである。
テキストボックスで数字のみ入力を許可し、それ以外の入力を禁止する場合は、ES_NUMBER
スタイルを使うとよい。スタイルというのは、ウィンドウやコントロールの振る舞いを変えるフラグ群の整数値である。
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
LTEXT "整数:", stc1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14, ES_NUMBER
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
EDITTEXT
の行に「, ES_NUMBER
」を追記して上書き保存する。ninja
を実行。これで数字以外を入力できなくなった。
MessageBoxW(hwnd, szText, L"Nibai", MB_ICONINFORMATION);
にL"Nibai"
というテキストがあるが、これを日本語化したい。しかしソースコードに直接日本語のL"二倍"
と書くのは、ソースコード互換性がない。
そこでリソースの「文字列テーブル」というものを使う。
- リソーエディタで
dialog_res.rc
を開く。 - 「編集」メニューの「追加」→「文字列テーブルを追加」を選ぶ。
- 「OK」ボタンを押す。文字列テーブル「
RT_STRING
」→「日本語」が追加される。 - 次のように編集する。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
STRINGTABLE
{
100, "二倍した結果"
}
二重引用符("
)は半角で入力しなければならない。リソーエディタのツールバーの一番左のボタン「再コンパイル」をクリックする。「再コンパイルしました。」と表示されたら成功。上書き保存する。これでリソース側の準備ができた。
文字列テーブルの文字列を読み込むにはLoadString
というAPI関数を使う。
int WINAPI LoadStringW (HINSTANCE hInstance, UINT uID, LPWSTR lpBuffer, int cchBufferMax);
しかし、LoadStringW
という関数には4つも引数がある。これを毎回呼び出すのは手間が掛かるので次のようなヘルパー関数LoadStringDx
を追加する。
LPWSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static WCHAR s_sz[4][cchBuffMax];
WCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadStringW(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
これでLoadStringDx(100)
のように呼ぶと100番の文字列を読み込むことができる。
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE) * 2;
WCHAR szText[64];
StringCbPrintfW(szText, sizeof(szText), L"%d", n);
MessageBoxW(hwnd, szText, LoadStringDx(100), MB_ICONINFORMATION);
EndDialog(hwnd, IDOK);
}
文字列テーブルには日本語バージョン以外に英語やフランス語バージョンなどを追加できるので、これで国際化ができるようになった。
ソース(dialog.cpp
)は次の通りである。
#include <windows.h>
#include <windowsx.h>
#include <strsafe.h>
static HICON s_hIcon = NULL;
static HICON s_hIconSmall = NULL;
LPWSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static WCHAR s_sz[4][cchBuffMax];
WCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadStringW(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HINSTANCE hinst = GetModuleHandle(NULL);
s_hIcon = LoadIcon(hinst, MAKEINTRESOURCE(1));
s_hIconSmall =
(HICON)LoadImage(hinst, MAKEINTRESOURCE(1), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), 0);
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)s_hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)s_hIconSmall);
return TRUE;
}
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE) * 2;
WCHAR szText[64];
StringCbPrintfW(szText, sizeof(szText), L"%d", n);
MessageBoxW(hwnd, szText, LoadStringDx(100), MB_ICONINFORMATION);
EndDialog(hwnd, IDOK);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
リソース(dialog_res.rc
)は次の通りである。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
LTEXT "整数:", stc1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14, ES_NUMBER
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_STRING
STRINGTABLE
{
100, "二倍した結果"
}
...(以下略)...
このままではダイアログが古臭く見える。特にボタンがダサい。
そこでマニュフェストと呼ばれるデータを追加し、comctl32.dll
のInitCommonControls
関数の呼び出しを追加する。
- リソーエディタで
dialog_res.rc
を開く。 - 「編集」メニューから「追加」→「マニフェストを追加」を選ぶ。
- 「リソースの名前」に「1」を入力して、「OK」ボタンを押す。
- 上書き保存する。
これでマニフェストを追加できた。次はInitCommonControls
関数の呼び出しを追加する。
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
InitCommonControls();
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
これでninja
を実行する。
おやおや、ビルドに失敗した。FAILED:
と書かれているから失敗に間違いない。error:
と書かれている箇所がエラーメッセージだ。エラーメッセージには行番号(ここでは72)が付いている。この場合、dialog.cpp
の72行目にエラーが発生している。
実はInitCommonControls
関数の呼び出しには#include <commctrl.h>
が必要であった。ソースファイルの最初の方に#include <commctrl.h>
を追記する。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <strsafe.h>
static HICON s_hIcon = NULL;
...
再びninja
を実行。ビルド成功。
dialog.exe
を実行すると、ダイアログの見た目が新しくなった。
うわ、凄い。いかしてるぜ。
マニフェストとInitCommonControls
を追加すれば、見た目がカッコ良くなる。わかったかな?
一部のAPI関数はANSI版とUnicode版に分かれている。CMakeではA/W
のいずれも指定しないときは、ANSI版が使われる。Unicode版を優先したい場合は次のように、CMakeLists.txt
にUNICODE
、_UNICODE
マクロを定義する。
cmake_minimum_required(VERSION 2.4)
project(dialog C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(dialog WIN32 dialog.cpp dialog_res.rc)
target_link_libraries(dialog PRIVATE comctl32)
add_executable
の前にadd_definitions(-DUNICODE -D_UNICODE)
を挿入すると、コンパイル時にUNICODE
、_UNICODE
マクロが定義される。
「OK」ボタンを押したときに、テキストボックスの内容を2倍にしたい場合は、OnOK
関数を次のようにする。
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE);
SetDlgItemInt(hwnd, edt1, n * 2, TRUE);
}
整数をセットするのに、SetDlgItemInt
というAPI関数を使っている。EndDialog
がないので、「OK」ボタンでは終了しない。終了するには「キャンセル」ボタンを押すことになる。
何度も「OK」ボタンを押すと、最初がゼロでない限り、2倍の2倍の2倍の……となって、急激に増加するだろう。
ここまでのソースをGitHubに掲載している。
インターネットに接続できない人たちのために、ここにも掲載しておく。
ソース(dialog.cpp
)は以下の通り。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <strsafe.h>
static HICON s_hIcon = NULL;
static HICON s_hIconSmall = NULL;
LPWSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static WCHAR s_sz[4][cchBuffMax];
WCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadStringW(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
BOOL OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HINSTANCE hinst = GetModuleHandle(NULL);
s_hIcon = LoadIcon(hinst, MAKEINTRESOURCE(1));
s_hIconSmall =
(HICON)LoadImage(hinst, MAKEINTRESOURCE(1), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), 0);
SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)s_hIcon);
SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)s_hIconSmall);
return TRUE;
}
void OnOK(HWND hwnd)
{
INT n = GetDlgItemInt(hwnd, edt1, NULL, TRUE);
SetDlgItemInt(hwnd, edt1, n * 2, TRUE);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
InitCommonControls();
DialogBox(hInstance, MAKEINTRESOURCE(1), NULL, DialogProc);
DestroyIcon(s_hIcon);
DestroyIcon(s_hIconSmall);
return 0;
}
リソース(dialog_res.rc
)は以下の通り。
// dialog_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_DIALOG
1 DIALOG 0, 0, 215, 135
CAPTION "サンプル ダイアログ"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
LTEXT "整数:", -1, 17, 17, 29, 14
EDITTEXT edt1, 57, 17, 60, 14, ES_NUMBER
DEFPUSHBUTTON "OK", IDOK, 35, 115, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 115, 115, 60, 14
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_MANIFEST
#ifndef MSVC
1 24 "res/1041_Manifest_1.manifest"
#endif
//////////////////////////////////////////////////////////////////////////////
// RT_STRING
STRINGTABLE
{
100, "二倍した結果"
}
...(以下略)...
CMakeLists.txt
は以下の通り。
cmake_minimum_required(VERSION 2.4)
project(dialog C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(dialog WIN32 dialog.cpp dialog_res.rc)
target_link_libraries(dialog PRIVATE comctl32)
それでは、もう少し冒険してメモ帳を作ってみよう。
今度はダイアログアプリではない、普通のウィンドウアプリなので少しややこしくなる。
c:\dev\cxx
にnotepad
というフォルダを作り、そこにCMakeLists.txt
、notepad.cpp
、notepad_res.rc
を配置する。
CMakeLists.txt
は次のような内容である。
cmake_minimum_required(VERSION 2.4)
project(notepad C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(notepad WIN32 notepad.cpp notepad_res.rc)
target_link_libraries(notepad PRIVATE comctl32)
リソースnotepad_res.rc
は、次のように作成する。
- リソーエディタを開く。
- リソースの名前が
1
のアイコンを追加する。 - リソースの名前が
1
のマニフェストを追加する。 - 名前を「
notepad_res.rc
」にしてc:\dev\cxx\notepad
に保存する。
notepad.cpp
は次のような内容である。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <cstdio>
#include <strsafe.h>
static const TCHAR s_szName[] = TEXT("My Notepad");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -2;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
ややこしいが、一つ一つ見ていこう。
static const TCHAR s_szName[] = TEXT("My Notepad");
これは名前である。ウィンドウの名前であり、ウィンドウクラス名の名前でもある。変更しないのでconst
キーワードを追加した。
static HINSTANCE s_hInst = NULL;
これはインスタンスのハンドルを格納する変数である。GetModuleHandle(NULL)
を呼ぶと返ってくるハンドルと同じである。これはWinMain
関数で初期化される。
static HWND s_hMainWnd = NULL;
これはメインウィンドウのハンドルを格納する変数である。
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
return TRUE;
}
これはWM_CREATE
メッセージのプロシージャである。ダイアログアプリではWM_INITDIALOG
メッセージが使われたが、ウィンドウアプリではWM_CREATE
を使う。
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
これはウィンドウが破棄されたときに呼ばれるWM_DESTROY
メッセージのプロシージャである。PostQuitMessage
はメッセージループを終了する(ここではアプリの終了を意味する)。
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
このWindowProc
関数はウィンドウプロシージャである。戻り値がINT_PTR
ではなくLRESULT
であることがダイアログプロシージャとは異なる。
また、default:
の処理でDefWindowProc
関数を呼んでいることに注意。
サブクラス化していないウィンドウプロシージャでは、既定の処理でDefWindowProc
を呼ぶ決まりになっている。
次はWinMain
関数の内部を見ていこう。
s_hInst = hInstance;
ここではhInstance
のハンドルを変数に保存している。
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
...
このコードはウィンドウのクラスを新しく登録している。ZeroMemory
関数で構造体をゼロでクリアし、構造体のメンバーを初期化している。wc.lpfnWndProc
にウィンドウプロシージャを指定。wc.hInstance
にモジュールのインスタンスを指定。wc.hIcon
に既定のアプリのアイコンを指定。wc.hCursor
に既定の矢印のカーソルを指定。wc.hbrBackground
に3Dのボタンの表面の色を指定する。wc.lpszClassName
にウィンドウクラス名を指定する。
構造体の初期化が終わったら、RegisterClass
関数を呼び、ウィンドウクラスを新しく登録する。
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
RegisterClass
で失敗したら、メッセージボックスを表示して終了する。
次はウィンドウの作成である。
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -2;
}
CreateWindow
関数で指定したウィンドウクラスのウィンドウを作成する。
ウィンドウのスタイルはWS_OVERLAPPEDWINDOW
スタイルである。
作成時の位置やサイズはピクセル単位で指定できるが、CW_USEDEFAULT
は特殊な値で、位置やサイズを指定しないという意味である。
作成に失敗したら、これもメッセージボックスを表示して終了する。
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
作成しただけでは表示されない。ShowWindow
関数とUpdateWindow
関数で表示させる。
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
これはメッセージループと呼ばれるものである。ここでメッセージを送信したり、処理したりする。PostQuitMessage
関数はこのメッセージループを終了させる。逆に言えば、PostQuitMessage
を使わないと、このようなウィンドウアプリは終了しない。
それではいつものようにビルドしてみよう。
cmake -G "Ninja"
ninja
ビルドが完了したら、試しに起動してみる。
何の変哲もないウィンドウが表示された。このウィンドウは、WS_OVERLAPPEDWINDOW
スタイルを指定したので、最小化したり、最大化したりできる。
メモ帳にするためには、EDIT
コントロールが必要だ。OnCreate
関数に次のように追記する。
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = ES_MULTILINE | ES_WANTRETURN | WS_HSCROLL | WS_VSCROLL |
WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hEdit = CreateWindowEx(exstyle, L"EDIT", NULL, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)edt1, s_hInst, NULL);
if (hEdit == NULL)
return FALSE;
return TRUE;
}
GetClientRect
関数でタイトルバーや枠線を除いたクライアント領域の長方形(RECT
構造体)を取得する。
次にCreateWindowEx
関数でEDIT
コントロールを作成する。CreateWindowEx
関数はCreateWindow
を拡張した関数で、拡張スタイルを指定できる。
WS_EX_CLIENTEDGE
はへこんだ枠線を描画する拡張スタイルである。ES_MULTILINE | ES_WANTRETURN | WS_HSCROLL | WS_VSCROLL | WS_CHILD | WS_VISIBLE
はEDIT
コントロールのスタイルである。
ES_MULTILINE
スタイルは複数行を意味する。WS_CHILD
スタイルは、子ウィンドウであることを意味する。WS_VISIBLE
スタイルはすぐに表示することを意味する。
WS_HSCROLL
とWS_VSCROLL
スタイルはスクロールバーの表示を表している。
CreateWindowEx
が失敗すれば、OnCreate
はFALSE
を返す。このとき親のメインウィンドウの作成は失敗する。
ninja
を再び実行して動作を確認しよう。
今度は文字が入力できるEDIT
コントロールが付いてきた。しかしサイズの指定に問題がある。メインウィンドウのサイズが変更されたら、子ウィンドウのEDIT
コントロールのサイズも変更されるようにしたい。
まずは、WindowProc
関数に次の行を追加する:
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
次に、OnSize
関数を追加する。MsgCrackでOnSize
と入力してEnter
キーでコピー。WindowProc
関数の定義の前に貼り付ける。そして次のようにコードを追記する。
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
HWND hEdit = GetDlgItem(hwnd, edt1);
MoveWindow(hEdit, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
これで親ウィンドウのサイズを変更したら、子ウィンドウのedt1
のサイズも変更されるようになる。GetDlgItem
は親ウィンドウハンドルとコントロールIDから子ウィンドウを取得する関数だ。MoveWindow
はウィンドウの位置とサイズを変更する関数である。
ここまでのソース(notepad.cpp
)は以下のようになる。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <cstdio>
#include <strsafe.h>
static const TCHAR s_szName[] = TEXT("My Notepad");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = ES_MULTILINE | ES_WANTRETURN | WS_HSCROLL | WS_VSCROLL |
WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hEdit = CreateWindowEx(exstyle, L"EDIT", NULL, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)edt1, s_hInst, NULL);
if (hEdit == NULL)
return FALSE;
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
HWND hEdit = GetDlgItem(hwnd, edt1);
MoveWindow(hEdit, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -2;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
メインアイコンをリソースに追加したんだから、既定のアイコンを使わなくてもいいだろう。次のように修正する。
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1));
メインメニューを追加しよう。
- リソーエディタで
notepad_res.rc
を開く。 - 「編集」メニューの「追加」→「メニューを追加」を選ぶ。
- 「リソースの名前」に「1」(いち)を指定して「OK」ボタンを押す。
RT_MENU
→1
→日本語
が追加される。 - 次のように書き換えて上書き保存する。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", 100
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", 101
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", 102
}
}
ソースも次のように書き換える。
wc.lpszMenuName = MAKEINTRESOURCE(1);
これでリソース名1
のメニューが自動的に読み込まれる。ninja
を実行して確かめてみよう。
メニューを追加したが、そのコマンドは定義されていないので、選択されても何も起こらない。メニューのコマンドを実装するには、WM_COMMAND
メッセージで処理を書かないといけない。
WindowProc
関数に次の行を追記する。
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
そしてOnCommand
関数の定義を次のように追記する。
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case 100:
// TODO:
break;
case 101:
// TODO:
break;
case 102:
DestroyWindow(hwnd);
break;
}
}
TODO:
と書いてあるものは後で実装するという意味である。コマンドIDの102
は「アプリを終了する」というコマンドであった。よってDestroyWindow
関数でメインウィンドウを破棄する。
ninja
を実行してnotepad
を再起動すると、メニューからアプリを終了できることが確認できる。
次はコマンドID 100
だ。コマンドIDの100
は、「ファイルを開く」という意味だった。次のようなDoLoad
という関数を追加する。
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
std::string str;
char buf[256];
if (FILE *fp = _wfopen(pszFile, L"rb"))
{
while (fgets(buf, 256, fp))
{
str += buf;
}
fclose(fp);
return SetDlgItemTextA(hwnd, edt1, str.c_str());
}
return FALSE;
}
C++のstd::string
を使うには#include <string>
が必要だった。これを追加する。さらに次のOnOpen
関数を追加する。
void OnOpen(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetOpenFileName(&ofn))
{
DoLoad(hwnd, szFile);
}
}
GetOpenFileName
関数でユーザに、開きたいファイルを問い合わせている。GetOpenFileName
を使うには#include <commdlg.h>
とcomdlg32.dll
へのリンクが必要だった。
ソースの最初の方に#include <commdlg.h>
を追記し、CMakeLists.txt
のtarget_link_libraries
にcomdlg32
を追記する。
OnCommand
関数にOnOpen
の呼び出しを追加する。
{
switch (id)
{
case 100:
OnOpen(hwnd);
break;
...
これでコマンドID 100
を実行すると、ファイルを開くことができる。ninja
を実行して試してみよう。
次は「名前を付けて保存」のコマンドID 101
である。DoSave
という関数
とOnSave
という関数を次のように追加する。
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
INT cch = GetWindowTextLengthA(hEdit);
std::string str;
str.resize(cch);
GetWindowTextA(hEdit, &str[0], cch + 1);
if (FILE *fp = _wfopen(pszFile, L"wb"))
{
size_t written = fwrite(str.c_str(), str.size(), 1, fp);
fclose(fp);
return written > 0;
}
return FALSE;
}
void OnSave(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetSaveFileName(&ofn))
{
DoSave(hwnd, szFile);
}
}
さらにOnCommand
関数にコマンド101
の処理を追加する。
case 101:
OnSave(hwnd);
break;
これで保存もできるようになった。
ここまでのソース(notepad.cpp
)は以下の通り。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <cstdio>
#include <string>
#include <strsafe.h>
static const TCHAR s_szName[] = TEXT("My Notepad");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = ES_MULTILINE | ES_WANTRETURN | WS_HSCROLL | WS_VSCROLL |
WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hEdit = CreateWindowEx(exstyle, L"EDIT", NULL, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)edt1, s_hInst, NULL);
if (hEdit == NULL)
return FALSE;
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
HWND hEdit = GetDlgItem(hwnd, edt1);
MoveWindow(hEdit, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
std::string str;
char buf[256];
if (FILE *fp = _wfopen(pszFile, L"rb"))
{
while (fgets(buf, 256, fp))
{
str += buf;
}
fclose(fp);
return SetDlgItemTextA(hwnd, edt1, str.c_str());
}
return FALSE;
}
void OnOpen(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetOpenFileName(&ofn))
{
DoLoad(hwnd, szFile);
}
}
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
INT cch = GetWindowTextLengthA(hEdit);
std::string str;
str.resize(cch);
GetWindowTextA(hEdit, &str[0], cch + 1);
if (FILE *fp = _wfopen(pszFile, L"wb"))
{
size_t written = fwrite(str.c_str(), str.size(), 1, fp);
fclose(fp);
return written > 0;
}
return FALSE;
}
void OnSave(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetSaveFileName(&ofn))
{
DoSave(hwnd, szFile);
}
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case 100:
OnOpen(hwnd);
break;
case 101:
OnSave(hwnd);
break;
case 102:
DestroyWindow(hwnd);
break;
}
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = MAKEINTRESOURCE(1);
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -2;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
ここまでのリソース(notepad_res.rc
)は次の通り。
// notepad_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_MENU
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", 100
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", 101
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", 102
}
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_MANIFEST
#ifndef MSVC
1 24 "res/1041_Manifest_1.manifest"
#endif
...(以下略)...
ここまでのCMakeLists.txt
は以下の通り。
cmake_minimum_required(VERSION 2.4)
project(notepad C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(notepad WIN32 notepad.cpp notepad_res.rc)
target_link_libraries(notepad PRIVATE comctl32 comdlg32)
メモ帳の使いやすさや親切さのため、細かい所を修正する。
Ctrl+O
、Ctrl+S
などのアクセスキーを実装する。- ファイルドロップでファイルを開けるようにする。
- ファイルを開くとき、保存するときに失敗したらエラーメッセージをちゃんと表示する。
- ウィンドウアクティブ時に
edt1
にフォーカスを当てる。 edt1
に等幅フォントを指定する。
アクセスキーは、リソースから追加できる。
- リソーエディタで
notepad_res.rc
を開く。 - 「編集」メニューから「追加」→「アクセスキーを追加」を選ぶ。
- 「リソースの名前」に「1」(いち)を指定して、「OK」ボタンを押す。
- 次のような内容になるよう編集し、上書き保存する。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
1 ACCELERATORS
{
"O", 100, CONTROL, VIRTKEY
"S", 101, CONTROL, VIRTKEY
}
このアクセスキーを有効にするには、メッセージループにちょっとしたトリックが必要である。
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(1));
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
if (TranslateAccelerator(s_hMainWnd, hAccel, &msg))
continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyAcceleratorTable(hAccel);
これでCtrl+O
やCtrl+S
が有効になった。
次は、ファイルドロップを実装する。まず、WM_CREATE
処理時に、ドロップを受け付けるように DragAcceptFiles(hwnd, TRUE);
を呼ぶ。
次にWM_DROPFILES
メッセージの処理を追加する。ウィンドウプロシージャWindowProc
にWM_DROPFILES
の処理を追加する。
HANDLE_MSG(hwnd, WM_DROPFILES, OnDropFiles);
OnDropFiles
関数は次の通りである。
void OnDropFiles(HWND hwnd, HDROP hdrop)
{
TCHAR szPath[MAX_PATH];
DragQueryFile(hdrop, 0, szPath, MAX_PATH);
DragFinish(hdrop);
DoLoad(hwnd, szPath);
}
これでテキストファイルがドラッグ&ドロップされたらそのファイルを開くようになる。
ファイル読み込み時と保存時のエラーメッセージだが、前述のLoadStringDx
関数と文字列テーブルを使って実装する。LoadStringDx
は次のような関数だった。
LPWSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static WCHAR s_sz[4][cchBuffMax];
WCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadStringW(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
文字列テーブルは次のようにする。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
STRINGTABLE
{
100, "ファイルの保存に失敗しました。"
101, "ファイルを開くのに失敗しました。"
}
これらを使えば、DoLoad
関数は次のようになる。
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
std::string str;
char buf[256];
if (FILE *fp = _wfopen(pszFile, L"rb"))
{
while (fgets(buf, 256, fp))
{
str += buf;
}
fclose(fp);
if (SetDlgItemTextA(hwnd, edt1, str.c_str()))
{
return TRUE;
}
}
MessageBox(hwnd, LoadStringDx(101), NULL, MB_ICONERROR);
return FALSE;
}
DoSave
関数は次のようになる。
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
INT cch = GetWindowTextLengthA(hEdit);
std::string str;
str.resize(cch);
GetWindowTextA(hEdit, &str[0], cch + 1);
if (FILE *fp = _wfopen(pszFile, L"wb"))
{
size_t written = fwrite(str.c_str(), str.size(), 1, fp);
fclose(fp);
if (written > 0)
return TRUE;
}
MessageBox(hwnd, LoadStringDx(100), NULL, MB_ICONERROR);
return FALSE;
}
これでエラーメッセージはバッチリだ。
アクティブ時のフォーカスは次のようにWM_ACTIVATE
メッセージを処理するとよい。
void OnActivate(HWND hwnd, UINT state, HWND hwndActDeact, BOOL fMinimized)
{
SetFocus(GetDlgItem(hwnd, edt1));
}
これでメモ帳を開いたときにすぐにedt1
に入力できる。
OnCreate
でedt1
に等幅フォントを設定しよう。まずはフォントハンドルを保持する変数s_hFont
を追加する。
static HFONT s_hFont = NULL;
次に、OnCreate
で等幅フォント作成と設定を行う。
LOGFONT lf = { -14 };
lf.lfPitchAndFamily = FIXED_PITCH | FF_MODERN;
lf.lfCharSet = SHIFTJIS_CHARSET;
s_hFont = CreateFontIndirect(&lf);
SetWindowFont(hEdit, s_hFont, TRUE);
これで等幅フォント作成と設定ができた。WinMain
の最後でs_hFont
を破棄する。
DeleteObject(s_hFont);
これでフォントの設定は完了した。
次は、メモ帳に「編集」メニューを付けてみよう。
まず、リソーエディタでnotepad_res.rc
を開き、「編集」メニューを次のように追加する。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", 100
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", 101
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", 102
}
POPUP "編集(&E)"
{
MENUITEM "元に戻す(&U)\tCtrl+Z", 103
MENUITEM SEPARATOR
MENUITEM "切り取り(&T)\tCtrl+X", 104
MENUITEM "コピー(&C)\tCtrl+C", 105
MENUITEM "貼り付け(&P)\tCtrl+V", 106
MENUITEM "削除(&D)\tDel", 107
MENUITEM SEPARATOR
MENUITEM "すべて選択(&A)\tCtrl+A", 108
}
}
上書き保存する。そして103
~108
までのコマンドを実装しよう。
OnCommand
関数を次のようにすれば実装完了だ。
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
switch (id)
{
case 100:
OnOpen(hwnd);
break;
case 101:
OnSave(hwnd);
break;
case 102:
DestroyWindow(hwnd);
break;
case 103:
SendMessage(hEdit, EM_UNDO, 0, 0);
break;
case 104:
SendMessage(hEdit, WM_CUT, 0, 0);
break;
case 105:
SendMessage(hEdit, WM_COPY, 0, 0);
break;
case 106:
SendMessage(hEdit, WM_PASTE, 0, 0);
break;
case 107:
SendMessage(hEdit, WM_CLEAR, 0, 0);
break;
case 108:
SendMessage(hEdit, EM_SETSEL, 0, -1);
break;
}
}
多くの機能は、SendMessage
関数でメッセージを送信するだけで利用可能だ。
アクセスキーのCtrl+X
、Ctrl+C
、Ctrl+V
、Del
、Ctrl+A
については、
EDIT
コントロールにすでに実装されているので追加しなくてもよい。
ninja
を実行してちゃんと動作するか確認しよう。
100
~108
のコマンドが実装されたが、番号だけでは何のことかわからない。
人間に分かりやすい識別子をコマンドIDにしてみよう。
- リソーエディタで
notepad_res.rc
を開く。 - 次に、「表示」メニューから「リソースIDの一覧」を選ぶ。「リソースIDの一覧」ウィンドウが開かれる。
- 「リソースIDの一覧」の中を右クリックして、「追加...」を選ぶ。「リソースIDの追加」ダイアログが開かれる。
- 「IDの名前」に「
ID_OPEN
」と入力し、「整数」に「100
」と入力して「OK」ボタンを押す。これで「ID_OPEN
」という名前でコマンドID100
が追加された。 - 同様にして
ID_SAVE
→101
、ID_EXIT
→102
、ID_UNDO
→103
、ID_CUT
→104
、ID_COPY
→105
、ID_PASTE
→106
、ID_DELETE
→107
、ID_SELECT_ALL
→108
を追加する。 - 上書き保存する。
resource.h
というファイルが作成される。 notepad.cpp
の上の方に#include "resource.h"
を追記する。- 次のように
OnCommand
のコマンドIDをすべて識別子に直す。
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
switch (id)
{
case ID_OPEN:
OnOpen(hwnd);
break;
case ID_SAVE:
OnSave(hwnd);
break;
case ID_EXIT:
DestroyWindow(hwnd);
break;
case ID_UNDO:
SendMessage(hEdit, EM_UNDO, 0, 0);
break;
case ID_CUT:
SendMessage(hEdit, WM_CUT, 0, 0);
break;
case ID_COPY:
SendMessage(hEdit, WM_COPY, 0, 0);
break;
case ID_PASTE:
SendMessage(hEdit, WM_PASTE, 0, 0);
break;
case ID_DELETE:
SendMessage(hEdit, WM_CLEAR, 0, 0);
break;
case ID_SELECT_ALL:
SendMessage(hEdit, EM_SETSEL, 0, -1);
break;
}
}
これでコマンドIDがだいぶ分かりやすくなった。
メモ帳に日時を表す文字列を挿入する機能を追加してみよう。
- リソーエディタで
notepad_res.rc
を開き、「リソースIDの一覧」ウィンドウを開く。 - 「リソースIDの一覧」の中を右クリックして、「追加...」を選ぶ。「リソースIDの追加」ダイアログが開かれる。
- 「IDの名前」に「
ID_INSERT_DATETIME
」と入力した後、「AUTO」ボタンを押してから「OK」ボタンを押す。これで「ID_INSERT_DATETIME
」というコマンドIDが追加される。 - 「編集」メニューに「日時の挿入(&I)」というメニュー項目を
ID_INSERT_DATETIME
というコマンドIDで追加する。 - 上書き保存する。
ID_INSERT_DATETIME
について次のようにコードを追記する。
void OnInsertDateTime(HWND hwnd)
{
TCHAR szText[64];
SYSTEMTIME st;
GetLocalTime(&st);
StringCbPrintf(szText, sizeof(szText), TEXT("%04u.%02u.%02u %02u:%02u:%02u"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
HWND hEdit = GetDlgItem(hwnd, edt1);
SendMessage(hEdit, EM_REPLACESEL, TRUE, (LPARAM)szText);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
switch (id)
{
...(中略)...
case ID_SELECT_ALL:
SendMessage(hEdit, EM_SETSEL, 0, -1);
break;
case ID_INSERT_DATETIME:
OnInsertDateTime(hwnd);
break;
}
}
EDIT
コントロールのEM_REPLACESEL
メッセージは、現在の選択範囲を指定したテキストに置き換える。
EDIT
コントロール固有のメッセージはEM_
で始まる決まりだ。
これでメニュー項目を選べば、現在の日時を挿入できる。
ここまでのソースコードをまとめとこう。
まず、ソース(notepad.cpp
)。
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <cstdio>
#include <string>
#include <strsafe.h>
#include "resource.h"
static const TCHAR s_szName[] = TEXT("My Notepad");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
static HFONT s_hFont = NULL;
LPWSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static WCHAR s_sz[4][cchBuffMax];
WCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadStringW(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = ES_MULTILINE | ES_WANTRETURN | WS_HSCROLL | WS_VSCROLL | WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hEdit = CreateWindowEx(exstyle, L"EDIT", NULL, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)edt1, s_hInst, NULL);
if (hEdit == NULL)
return FALSE;
DragAcceptFiles(hwnd, TRUE);
LOGFONT lf = { -14 };
lf.lfPitchAndFamily = FIXED_PITCH | FF_MODERN;
lf.lfCharSet = SHIFTJIS_CHARSET;
s_hFont = CreateFontIndirect(&lf);
SetWindowFont(hEdit, s_hFont, TRUE);
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
HWND hEdit = GetDlgItem(hwnd, edt1);
MoveWindow(hEdit, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
std::string str;
char buf[256];
if (FILE *fp = _wfopen(pszFile, L"rb"))
{
while (fgets(buf, 256, fp))
{
str += buf;
}
fclose(fp);
if (SetDlgItemTextA(hwnd, edt1, str.c_str()))
{
return TRUE;
}
}
MessageBox(hwnd, LoadStringDx(101), NULL, MB_ICONERROR);
return FALSE;
}
void OnOpen(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetOpenFileName(&ofn))
{
DoLoad(hwnd, szFile);
}
}
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
INT cch = GetWindowTextLengthA(hEdit);
std::string str;
str.resize(cch);
GetWindowTextA(hEdit, &str[0], cch + 1);
if (FILE *fp = _wfopen(pszFile, L"wb"))
{
size_t written = fwrite(str.c_str(), str.size(), 1, fp);
fclose(fp);
if (written > 0)
return TRUE;
}
MessageBox(hwnd, LoadStringDx(100), NULL, MB_ICONERROR);
return FALSE;
}
void OnSave(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("txt");
if (GetSaveFileName(&ofn))
{
DoSave(hwnd, szFile);
}
}
void OnInsertDateTime(HWND hwnd)
{
TCHAR szText[64];
SYSTEMTIME st;
GetLocalTime(&st);
StringCbPrintf(szText, sizeof(szText), TEXT("%04u.%02u.%02u %02u:%02u:%02u"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
HWND hEdit = GetDlgItem(hwnd, edt1);
SendMessage(hEdit, EM_REPLACESEL, TRUE, (LPARAM)szText);
}
void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
HWND hEdit = GetDlgItem(hwnd, edt1);
switch (id)
{
case ID_OPEN:
OnOpen(hwnd);
break;
case ID_SAVE:
OnSave(hwnd);
break;
case ID_EXIT:
DestroyWindow(hwnd);
break;
case ID_UNDO:
SendMessage(hEdit, EM_UNDO, 0, 0);
break;
case ID_CUT:
SendMessage(hEdit, WM_CUT, 0, 0);
break;
case ID_COPY:
SendMessage(hEdit, WM_COPY, 0, 0);
break;
case ID_PASTE:
SendMessage(hEdit, WM_PASTE, 0, 0);
break;
case ID_DELETE:
SendMessage(hEdit, WM_CLEAR, 0, 0);
break;
case ID_SELECT_ALL:
SendMessage(hEdit, EM_SETSEL, 0, -1);
break;
case ID_INSERT_DATETIME:
OnInsertDateTime(hwnd);
break;
}
}
void OnDropFiles(HWND hwnd, HDROP hdrop)
{
TCHAR szPath[MAX_PATH];
DragQueryFile(hdrop, 0, szPath, MAX_PATH);
DragFinish(hdrop);
DoLoad(hwnd, szPath);
}
void OnActivate(HWND hwnd, UINT state, HWND hwndActDeact, BOOL fMinimized)
{
SetFocus(GetDlgItem(hwnd, edt1));
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_DROPFILES, OnDropFiles);
HANDLE_MSG(hwnd, WM_ACTIVATE, OnActivate);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = MAKEINTRESOURCE(1);
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -2;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(1));
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
if (TranslateAccelerator(s_hMainWnd, hAccel, &msg))
continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyAcceleratorTable(hAccel);
DeleteObject(s_hFont);
return 0;
}
次はリソース(notepad_res.rc
)。
// notepad_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#include "resource.h"
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_MENU
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", ID_OPEN
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", ID_SAVE
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", ID_EXIT
}
POPUP "編集(&E)"
{
MENUITEM "元に戻す(&U)\tCtrl+Z", ID_UNDO
MENUITEM SEPARATOR
MENUITEM "切り取り(&T)\tCtrl+X", ID_CUT
MENUITEM "コピー(&C)\tCtrl+C", ID_COPY
MENUITEM "貼り付け(&P)\tCtrl+V", ID_PASTE
MENUITEM "削除(&D)\tDel", ID_DELETE
MENUITEM SEPARATOR
MENUITEM "すべて選択(&A)\tCtrl+A", ID_SELECT_ALL
MENUITEM "日時の挿入(&I)", ID_INSERT_DATETIME
}
}
//////////////////////////////////////////////////////////////////////////////
// RT_ACCELERATOR
1 ACCELERATORS
{
"O", ID_OPEN, CONTROL, VIRTKEY
"S", ID_SAVE, CONTROL, VIRTKEY
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_MANIFEST
#ifndef MSVC
1 24 "res/1041_Manifest_1.manifest"
#endif
//////////////////////////////////////////////////////////////////////////////
// RT_STRING
STRINGTABLE
{
100, "ファイルの保存に失敗しました。"
101, "ファイルを開くのに失敗しました。"
}
...(以下略)...
そしてresource.h
。
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ Compatible
// This file is automatically generated by RisohEditor.
// notepad_res.rc
#define ID_OPEN 100
#define ID_SAVE 101
#define ID_EXIT 102
#define ID_UNDO 103
#define ID_CUT 104
#define ID_COPY 105
#define ID_PASTE 106
#define ID_DELETE 107
#define ID_SELECT_ALL 108
#define ID_INSERT_DATETIME 109
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 100
#define _APS_NEXT_COMMAND_VALUE 110
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 300
#endif
#endif
最後にCMakeLists.txt
。
cmake_minimum_required(VERSION 2.4)
project(notepad C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(notepad WIN32 notepad.cpp notepad_res.rc)
target_link_libraries(notepad PRIVATE comctl32 comdlg32)
次はお絵かきソフト「ペイント」を作ろう。マウスでお絵かきできるかな。
「ペイント」の初期のソースをここに掲載する。
最初にヘッダファイルpaint.h
は次の通り。
#pragma once
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <cstdio>
#include <string>
#include <strsafe.h>
#include "resource.h"
LPTSTR LoadStringDx(INT nID);
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
HBITMAP DoCreate24BppBitmap(INT cx, INT cy);
HBITMAP DoGetSubImage(HBITMAP hbm, const RECT *prc);
void DoPutSubImage(HBITMAP hbm, const RECT *prc, HBITMAP hbmSubImage);
HBITMAP LoadBitmapFromFile(LPCTSTR bmp_file);
BOOL SaveBitmapToFile(LPCTSTR bmp_file, HBITMAP hbm);
HGLOBAL DIBFromBitmap(HBITMAP hbm);
次にソース(paint.cpp)。
#include "paint.h"
static const TCHAR s_szName[] = TEXT("My Paint");
static const TCHAR s_szCanvasName[] = TEXT("My Paint Canvas");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
static HWND s_hCanvasWnd = NULL;
LPTSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static TCHAR s_sz[4][cchBuffMax];
TCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadString(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
static BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = WS_HSCROLL | WS_VSCROLL | WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hCanvas = CreateWindowEx(exstyle, s_szCanvasName, s_szCanvasName, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)ctl1, s_hInst, NULL);
if (hCanvas == NULL)
return FALSE;
s_hCanvasWnd = hCanvas;
DragAcceptFiles(hwnd, TRUE);
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
MoveWindow(s_hCanvasWnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
// TODO:
MessageBox(hwnd, LoadStringDx(101), NULL, MB_ICONERROR);
return FALSE;
}
static void OnOpen(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("bmp");
if (GetOpenFileName(&ofn))
{
DoLoad(hwnd, szFile);
}
}
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
// TODO:
MessageBox(hwnd, LoadStringDx(100), NULL, MB_ICONERROR);
return FALSE;
}
static void OnSave(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("bmp");
if (GetSaveFileName(&ofn))
{
DoSave(hwnd, szFile);
}
}
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_OPEN:
OnOpen(hwnd);
break;
case ID_SAVE:
OnSave(hwnd);
break;
case ID_EXIT:
DestroyWindow(hwnd);
break;
case ID_CUT:
case ID_COPY:
case ID_PASTE:
case ID_DELETE:
case ID_SELECT_ALL:
case ID_SELECT:
case ID_PENCIL:
SendMessage(s_hCanvasWnd, WM_COMMAND, id, 0);
break;
}
}
static void OnDropFiles(HWND hwnd, HDROP hdrop)
{
TCHAR szPath[MAX_PATH];
DragQueryFile(hdrop, 0, szPath, MAX_PATH);
DragFinish(hdrop);
DoLoad(hwnd, szPath);
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_DROPFILES, OnDropFiles);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = MAKEINTRESOURCE(1);
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
ZeroMemory(&wc, sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
wc.lpfnWndProc = CanvasWndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = GetStockBrush(GRAY_BRUSH);
wc.lpszClassName = s_szCanvasName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -2;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -3;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(1));
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
if (TranslateAccelerator(s_hMainWnd, hAccel, &msg))
continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyAcceleratorTable(hAccel);
return 0;
}
次はcanvas.cpp
。
#include "paint.h"
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_CUT:
// TODO:
break;
case ID_COPY:
// TODO:
break;
case ID_PASTE:
// TODO:
break;
case ID_DELETE:
// TODO:
break;
case ID_SELECT_ALL:
// TODO:
break;
case ID_SELECT:
// TODO:
break;
case ID_PENCIL:
// TODO:
break;
}
}
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
その次はbitmap.cpp
。
#include <windows.h>
#include <windowsx.h>
#define BM_MAGIC 0x4D42 /* 'BM' */
typedef struct tagKHMZ_BITMAPINFOEX
{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[256];
} KHMZ_BITMAPINFOEX, FAR *LPKHMZ_BITMAPINFOEX;
HBITMAP DoCreate24BppBitmap(INT cx, INT cy)
{
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = cx;
bmi.bmiHeader.biHeight = cy;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
HDC hDC = CreateCompatibleDC(NULL);
LPVOID pvBits;
HBITMAP ret = CreateDIBSection(hDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
DeleteDC(hDC);
return ret;
}
HBITMAP DoGetSubImage(HBITMAP hbm, const RECT *prc)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
HBITMAP ret = DoCreate24BppBitmap(cx, cy);
if (!ret)
return NULL;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, ret);
BitBlt(hMemDC2, 0, 0, cx, cy, hMemDC1, prc->left, prc->top, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
return ret;
}
void DoPutSubImage(HBITMAP hbm, const RECT *prc, HBITMAP hbmSubImage)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (hbmSubImage)
{
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, hbmSubImage);
BitBlt(hMemDC1, prc->left, prc->top, cx, cy, hMemDC2, 0, 0, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
}
else
{
PatBlt(hMemDC1, prc->left, prc->top, cx, cy, BLACKNESS);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
}
HBITMAP LoadBitmapFromFile(LPCTSTR bmp_file)
{
HANDLE hFile;
BITMAPFILEHEADER bf;
KHMZ_BITMAPINFOEX bmi;
DWORD cb, cbImage;
LPVOID pvBits1, pvBits2;
HDC hDC;
HBITMAP hbm;
/* LoadImage can load a bottom-up bitmap */
hbm = (HBITMAP)LoadImage(NULL, bmp_file, IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE | LR_CREATEDIBSECTION);
if (hbm)
return hbm;
/* Let's load a top-down bitmap */
hFile = CreateFile(bmp_file, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
/* failed to open the file */
return NULL;
}
if (!ReadFile(hFile, &bf, sizeof(BITMAPFILEHEADER), &cb, NULL))
{
/* failed to read the file */
CloseHandle(NULL);
return NULL;
}
/* allocate the bits and read from file */
pvBits1 = NULL;
if (bf.bfType == BM_MAGIC && bf.bfReserved1 == 0 && bf.bfReserved2 == 0 &&
bf.bfSize > bf.bfOffBits && bf.bfOffBits > sizeof(BITMAPFILEHEADER) &&
bf.bfOffBits <= sizeof(BITMAPFILEHEADER) + sizeof(KHMZ_BITMAPINFOEX))
{
cbImage = bf.bfSize - bf.bfOffBits;
pvBits1 = HeapAlloc(GetProcessHeap(), 0, cbImage);
if (pvBits1)
{
if (!ReadFile(hFile, &bmi, bf.bfOffBits -
sizeof(BITMAPFILEHEADER), &cb, NULL) ||
!ReadFile(hFile, pvBits1, cbImage, &cb, NULL))
{
/* failed to read the file */
HeapFree(GetProcessHeap(), 0, pvBits1);
pvBits1 = NULL;
}
}
}
/* close the file */
CloseHandle(hFile);
if (pvBits1 == NULL)
{
return NULL; /* allocation failure */
}
/* create the DIB object and get the handle */
hbm = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS,
&pvBits2, NULL, 0);
if (hbm)
{
hDC = CreateCompatibleDC(NULL);
if (!SetDIBits(hDC, hbm, 0, (UINT)labs(bmi.bmiHeader.biHeight),
pvBits1, (BITMAPINFO*)&bmi, DIB_RGB_COLORS))
{
/* failed to get the bits */
DeleteObject(hbm);
hbm = NULL;
}
DeleteDC(hDC);
}
/* clean up */
HeapFree(GetProcessHeap(), 0, pvBits1);
return hbm;
}
/* save the bitmap to a BMP/DIB file */
BOOL SaveBitmapToFile(LPCTSTR bmp_file, HBITMAP hbm)
{
BOOL fOK;
BITMAPFILEHEADER bf;
KHMZ_BITMAPINFOEX bmi;
BITMAPINFOHEADER *pbmih;
DWORD cb, cbColors;
HDC hDC;
HANDLE hFile;
LPVOID pBits;
BITMAP bm;
/* verify the bitmap */
if (!GetObject(hbm, sizeof(BITMAP), &bm))
return FALSE;
pbmih = &bmi.bmiHeader;
ZeroMemory(pbmih, sizeof(BITMAPINFOHEADER));
pbmih->biSize = sizeof(BITMAPINFOHEADER);
pbmih->biWidth = bm.bmWidth;
pbmih->biHeight = bm.bmHeight;
pbmih->biPlanes = 1;
pbmih->biBitCount = bm.bmBitsPixel;
pbmih->biCompression = BI_RGB;
pbmih->biSizeImage = (DWORD)(bm.bmWidthBytes * bm.bmHeight);
/* size of color table */
if (bm.bmBitsPixel <= 8)
cbColors = (DWORD)((1ULL << bm.bmBitsPixel) * sizeof(RGBQUAD));
else
cbColors = 0;
bf.bfType = BM_MAGIC;
bf.bfReserved1 = 0;
bf.bfReserved2 = 0;
cb = sizeof(BITMAPFILEHEADER) + pbmih->biSize + cbColors;
bf.bfOffBits = cb;
bf.bfSize = cb + pbmih->biSizeImage;
/* allocate the bits */
pBits = HeapAlloc(GetProcessHeap(), 0, pbmih->biSizeImage);
if (pBits == NULL)
return FALSE; /* allocation failure */
/* create a DC */
hDC = CreateCompatibleDC(NULL);
/* get the bits */
fOK = FALSE;
if (GetDIBits(hDC, hbm, 0, (UINT)bm.bmHeight, pBits, (BITMAPINFO *)&bmi,
DIB_RGB_COLORS))
{
/* create the file */
hFile = CreateFile(bmp_file, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_WRITE_THROUGH, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
/* write to file */
fOK = WriteFile(hFile, &bf, sizeof(bf), &cb, NULL) &&
WriteFile(hFile, &bmi, sizeof(BITMAPINFOHEADER) + cbColors, &cb, NULL) &&
WriteFile(hFile, pBits, pbmih->biSizeImage, &cb, NULL);
/* close the file */
CloseHandle(hFile);
/* if writing failed, delete the file */
if (!fOK)
{
DeleteFile(bmp_file);
}
}
}
/* delete the DC */
DeleteDC(hDC);
/* clean up */
HeapFree(GetProcessHeap(), 0, pBits);
return fOK;
}
HGLOBAL DIBFromBitmap(HBITMAP hbm)
{
BITMAP bm;
GetObject(hbm, sizeof(bm), &bm);
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = bm.bmWidth;
bmi.bmiHeader.biHeight = bm.bmHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
DWORD cbBits = bm.bmWidthBytes * bm.bmHeight;
DWORD cbSize = sizeof(BITMAPINFOHEADER) + cbBits;
HGLOBAL ret = GlobalAlloc(GHND | GMEM_SHARE, cbSize);
if (!ret)
return NULL;
LPBYTE pb = (LPBYTE)GlobalLock(ret);
CopyMemory(pb, &bmi, sizeof(BITMAPINFOHEADER));
CopyMemory(pb + sizeof(BITMAPINFOHEADER), bm.bmBits, cbBits);
GlobalUnlock(ret);
return ret;
}
お次はresource.h
。
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ Compatible
// This file is automatically generated by RisohEditor.
// paint_res.rc
#define ID_OPEN 100
#define ID_SAVE 101
#define ID_EXIT 102
#define ID_CUT 103
#define ID_COPY 104
#define ID_PASTE 105
#define ID_DELETE 106
#define ID_SELECT_ALL 107
#define ID_SELECT 108
#define ID_PENCIL 109
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 100
#define _APS_NEXT_COMMAND_VALUE 110
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 300
#endif
#endif
お次はCMakeLists.txt
。
cmake_minimum_required(VERSION 2.4)
project(paint C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(paint WIN32 paint.cpp canvas.cpp bitmap.cpp paint_res.rc)
target_link_libraries(paint PRIVATE comctl32 comdlg32)
最後にpaint_res.rc
。
// paint_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#include "resource.h"
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_MENU
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", ID_OPEN
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", ID_SAVE
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", ID_EXIT
}
POPUP "編集(&E)"
{
MENUITEM "切り取り(&T)\tCtrl+X", ID_CUT
MENUITEM "コピー(&C)\tCtrl+C", ID_COPY
MENUITEM "貼り付け(&P)\tCtrl+V", ID_PASTE
MENUITEM "削除(&D)\tDel", ID_DELETE
MENUITEM SEPARATOR
MENUITEM "すべて選択(&A)\tCtrl+A", ID_SELECT_ALL
}
POPUP "ツール(&T)"
{
MENUITEM "選択(&S)", ID_SELECT
MENUITEM "鉛筆(&P)", ID_PENCIL
}
}
//////////////////////////////////////////////////////////////////////////////
// RT_ACCELERATOR
1 ACCELERATORS
{
"O", ID_OPEN, CONTROL, VIRTKEY
"S", ID_SAVE, CONTROL, VIRTKEY
"X", ID_CUT, CONTROL, VIRTKEY
"C", ID_COPY, CONTROL, VIRTKEY
"V", ID_PASTE, CONTROL, VIRTKEY
"A", ID_SELECT_ALL, CONTROL, VIRTKEY
VK_DELETE, ID_DELETE, VIRTKEY
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_MANIFEST
#ifndef MSVC
1 24 "res/1041_Manifest_1.manifest"
#endif
//////////////////////////////////////////////////////////////////////////////
// RT_STRING
STRINGTABLE
{
100, "ファイルの保存に失敗しました。"
101, "ファイルを開くのに失敗しました。"
}
...(以下略)...
cmake -G "Ninja" .
ninja
paint
次のようなウィンドウが開かれる。
WinMain
関数で2つのウィンドウクラスを登録している。
一つはメインウィンドウ、もう一つはキャンバス用の子ウィンドウ。
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
この CS_HREDRAW | CS_VREDRAW
は、「サイズ変更されたら再描画せよ」という意味である。
CS_DBLCLKS
は「ダブルクリックを認識せよ」という意味である。
wc.lpfnWndProc = CanvasWndProc;
ここのCanvasWndProc
は、キャンバスのウィンドウプロシージャであり、
ヘッダで宣言され、canvas.c
で定義されている。
wc.hbrBackground = GetStockBrush(GRAY_BRUSH);
このhbrBackground
は、背景を塗りつぶすブラシであり、ここでは灰色を選んでいる。
続いて
OnCreate
、
OnDestroy
、
OnSize
、
OnCommand
、
OnDropFiles
、
WindowProc
については、前回とほとんど同じで、解説は要らないだろう。
bitmap.cpp
ではビットマップという画像データを操作する関数を記述している。
ビットマップとは、画像のイメージをピクセル(画素)で記録した、Windowsの基本的な画像データである。
ビットマップ ファイルというのは、拡張子.bmp
のビットマップの画像ファイルのことである。
ビットマップ オブジェクトというのは、ファイルになっていない、メモリー上の画像データである。
ビットマップ オブジェクトは次のような関数を使えば作成できる。
HBITMAP DoCreate24BppBitmap(INT cx, INT cy)
{
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = cx;
bmi.bmiHeader.biHeight = cy;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
HDC hDC = CreateCompatibleDC(NULL);
LPVOID pvBits;
HBITMAP ret = CreateDIBSection(hDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
DeleteDC(hDC);
return ret;
}
この関数は、幅cx
、高さcy
の24bppのビットマップ オブジェクトを作成する。
24bppというのは24 bits per pixel、すなわち1ピクセルに24ビットを消費するフルカラーのビットマップ画像のことである。
DoCreate24BppBitmap
という関数名にDo
(ドゥ)が付いているのは、
Do
から始まるAPI関数は「ない」から、API関数と区別しやすいからである。
次のDoGetSubImage
は画像の一部分を取り出す関数である。
HBITMAP DoGetSubImage(HBITMAP hbm, const RECT *prc)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
HBITMAP ret = DoCreate24BppBitmap(cx, cy);
if (!ret)
return NULL;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, ret);
BitBlt(hMemDC2, 0, 0, cx, cy, hMemDC1, prc->left, prc->top, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
return ret;
}
RECT
構造体は、長方形領域を表し、
left
、
top
、
right
、
bottom
というLONG
型のメンバーを持つ。
HDC
は、デバイスコンテキストと呼ばれるもののハンドルである。デバイスコンテキストは
画面やプリンタ、画像データなどに関連付けられており、SelectBitmap
でビットマップを選択し、
BitBlt
関数で画像を転送すれば、画像データがその装置や画像データに描画される仕組みになっている。
次のDoPutSubImage
関数は、画像を別の画像に貼り付ける関数である。
void DoPutSubImage(HBITMAP hbm, const RECT *prc, HBITMAP hbmSubImage)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (hbmSubImage)
{
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, hbmSubImage);
BitBlt(hMemDC1, prc->left, prc->top, cx, cy, hMemDC2, 0, 0, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
}
else
{
PatBlt(hMemDC1, prc->left, prc->top, cx, cy, BLACKNESS);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
}
LoadBitmapFromFile
は、BMPファイルからビットマップを読み込む関数である。
SaveBitmapToFile
は、ビットマップをBMPファイルに保存する関数である。
ここでは、「キャンバス」とはメインウィンドウの子ウィンドウであり、直接、画像を表示するウィンドウである。
キャンバスの実装はcanvas.cpp
に記述することにする。まず、ビットマップハンドルを保存する変数を用意する。
HBITMAP g_hbm = NULL;
このg_hbm
を他の場所でも使えるように、paint.h
に
extern HBITMAP g_hbm;
と書き加える。
次のコードにより、キャンバスウィンドウ作成時に適当な大きさのビットマップを作成する。
static BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
g_hbm = DoCreate24BppBitmap(320, 120);
return TRUE;
}
ビットマップを作成したら後で破棄しないといけない。キャンバスウィンドウのWM_DESTROY
で破棄する。
static void OnDestroy(HWND hwnd)
{
DeleteObject(g_hbm);
g_hbm = NULL;
}
OnCreate
とOnDestroy
は、ちゃんとCanvasWndProc
で呼ばれるようにする。
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
次は、ビットマップを画面に描画する。キャンバスウィンドウのWM_PAINT
で次のように
描画する。
static void OnPaint(HWND hwnd)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
BitBlt(hDC, 0, 0, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY);
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
}
EndPaint(hwnd, &ps);
}
}
WM_PAINT
の処理で描画を行うときは、描画コードをBeginPaint
とEndPaint
で挟み込む。
BeginPaint
は、HDC
(デバイス コンテキストのハンドル)を返す。
このデバイスコンテキストというものは、画面や装置の一部の描画対象を表している。
これをうまく使えば、ウィンドウに描画することができる。
CreateCompatibleDC(NULL)
はもう一つ別のデバイス コンテキスト(DC)を返す。これはメモリー上のDCである。
CreateCompatibleDC(NULL)
を呼んだら、後でその戻り値をDeleteDC
で破棄しないといけない。
hMemDC
でビットマップのハンドル(hbm
)をSelectBitmap
関数で選択して、BitBlt
でビット群をhDC
に転送すれば、
ビットイメージがhDC
に転送される。これが画面への描画を引き起こす。
ビットマップを選択した後は、もう一度SelectBitmap
を呼んで選択を元に戻した方がよい。
OnCreate
、OnDestroy
、OnPaint
は、ちゃんとCanvasWndProc
から呼ばれるように
しないといけない。
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_PAINT, OnPaint);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
ninja
して確認しよう。
横320x縦120ピクセルの黒い画像が表示されているのが分かる。
ここまでのソースを次に掲載する。
マウスで線を引けるようにするには、
WM_LBUTTONDOWN
、
WM_MOUSEMOVE
、
WM_LBUTTONUP
メッセージを処理しないといけない。
WM_LBUTTONDOWN
は、クライアント領域でマウスの左ボタンをクリックしたときに発生する。
WM_MOUSEMOVE
は、マウスを移動させたときに発生する。
WM_LBUTTONUP
は、マウスの左ボタンを離したときに発生する。
これらを実装する。
static BOOL s_bDragging = FALSE;
static POINT s_ptOld;
...(中略)...
static void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
{
if (fDoubleClick)
return;
s_bDragging = TRUE;
SetCapture(hwnd);
s_ptOld.x = x;
s_ptOld.y = y;
}
static void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, x, y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_ptOld = pt;
}
static void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, x, y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
ReleaseCapture();
s_bDragging = FALSE;
}
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_PAINT, OnPaint);
HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
HANDLE_MSG(hwnd, WM_MOUSEMOVE, OnMouseMove);
HANDLE_MSG(hwnd, WM_LBUTTONUP, OnLButtonUp);
case WM_CAPTURECHANGED:
s_bDragging = FALSE;
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
一つ一つ見ていこう。
s_bDragging
は、現在マウスドラッグ中かを表す変数である。
ドラッグとは、ボタンを押しながら引きずるようなマウスの操作のことである。
ここでは、ドラッグで白い線を引くことを想定している。
s_ptOld
は、POINT
構造体で一つ前の位置を表している。
OnLButtonDown
では、s_bDragging
をTRUE
にして、SetCapture
関数を呼ぶことでマウスドラッグを開始する。
そして位置をs_ptOld
に保存している。SetCapture
を呼ぶと、マウスの動きの追跡を開始して、マウスポインタが
ウィンドウから離れてもメッセージを吸い取るようになる(キャプチャー)。
次にOnMouseMove
では、s_bDragging
がTRUEならば、
白いペンを使って、一つ前の位置から現在の位置まで、ビットマップ上に白い線を引いている。
MoveToEx
関数は、現在の描画位置を移動して、LineTo
関数は実際に線を引く関数である。
InvalidateRect
は、ウィンドウの再描画を引き起こす。
そしてs_ptOld
を更新している。
それからOnLButtonUp
では、ReleaseCapture
でキャプチャーを解放してドラッグを終了する。
WM_CAPTURECHANGED
は、キャプチャがEsc
キーなどでキャンセルされたときに呼ばれる。
WM_CAPTURECHANGED
メッセージはHANDLE_MSG
で対応されていないのでこのようになる。
これで線が引けるはずである。ninja
して確認しよう。
ここまでのソースを次のリンクに掲載する。
さて、ファイルの読み書きはpaint.cpp
のDoLoad
とDoSave
関数で行っていた。
次のようにする。
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
if (HBITMAP hbm = LoadBitmapFromFile(pszFile))
{
DeleteBitmap(g_hbm);
g_hbm = hbm;
InvalidateRect(s_hCanvasWnd, NULL, TRUE);
return TRUE;
}
MessageBox(hwnd, LoadStringDx(101), NULL, MB_ICONERROR);
return FALSE;
}
...(中略)...
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
if (SaveBitmapToFile(pszFile, g_hbm))
return TRUE;
MessageBox(hwnd, LoadStringDx(100), NULL, MB_ICONERROR);
return FALSE;
}
これでビットマップの読み書きができるようになったはずだ。
ninja
して動作を確認しよう。
ここまでのソースを次のリンクに掲載する。
選択したいときと、描画したいときを分けるために、「モード」というものを導入する。 選択したいときは「選択モード」、線を描画したいときは「線描画モード」というように、 モードによって動作を変更するようにしたい。
そこでモード変数というものを追加する。paint.h
に次の列挙型MODE
を追加し、
グローバル変数のg_nMode
も追加する。
enum MODE
{
MODE_SELECT,
MODE_PENCIL
};
extern MODE g_nMode;
そしてcanvas.cpp
の最初の方に次を追記する。
MODE g_nMode = MODE_PENCIL;
static POINT s_pt;
...
void DoNormalizeRect(RECT *prc)
{
if (prc->left > prc->right)
std::swap(prc->left, prc->right);
if (prc->top > prc->bottom)
std::swap(prc->top, prc->bottom);
}
このDoNormalizeRect
関数は、長方形を表すRECT
構造体を整える関数であり、
左(left
)より右(right
)が右側に来るようにして、
上(top
)より下(bottom
)が下側に来るようにしている。
また、OnPaint
とOnMouseMove
とOnLButtonUp
の動作をモードに合わせないといけない。
OnPaint
を次のようにする。
static void OnPaint(HWND hwnd)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
BitBlt(hDC, 0, 0, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY);
SelectBitmap(hMemDC, hbmOld);
if (g_nMode == MODE_SELECT)
{
RECT rc = { s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y };
DoNormalizeRect(&rc);
DrawFocusRect(hDC, &rc);
}
DeleteDC(hMemDC);
}
EndPaint(hwnd, &ps);
}
}
OnMouseMove
を次のようにする。
static void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
if (g_nMode == MODE_PENCIL)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, x, y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_ptOld = pt;
}
else
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
}
OnLButtonUp
を次のようにする。
static void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
if (g_nMode == MODE_PENCIL)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, x, y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
}
else
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
ReleaseCapture();
s_bDragging = FALSE;
}
また、「ツール」メニューでモードを切り替えられるようにする。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_SELECT_ALL:
// TODO:
break;
case ID_SELECT:
g_nMode = MODE_SELECT;
break;
case ID_PENCIL:
g_nMode = MODE_PENCIL;
break;
}
}
これで選択モードは実装できた。ninja
して試してみよう。
ここまでのソースを次のリンクに掲載する。
選択モードにすれば矩形選択ができることが確認できる。 次は、選択した領域に対する処理を実装する。
まずは、削除だが、領域を黒く塗ることで実装する。
static void OnDelete(HWND hwnd)
{
if (g_nMode != MODE_SELECT)
return;
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
DoPutSubImage(g_hbm, &rc, NULL);
InvalidateRect(hwnd, NULL, TRUE);
}
...(中略)...
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_CUT:
...(中略)...
break;
case ID_DELETE:
OnDelete(hwnd);
break;
...(中略)...
}
}
これで削除が実装できた。
ここまでのソースを次のリンクに掲載する。
次はコピーだ。クリップボードにビットマップをセットすることで実現する。
static void OnCopy(HWND hwnd)
{
RECT rc;
if (g_nMode == MODE_SELECT)
{
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
}
else
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
SetRect(&rc, 0, 0, bm.bmWidth, bm.bmHeight);
}
HBITMAP hbmNew = DoGetSubImage(g_hbm, &rc);
if (!hbmNew)
return;
HGLOBAL hDIB = DIBFromBitmap(hbmNew);
if (OpenClipboard(hwnd))
{
EmptyClipboard();
SetClipboardData(CF_DIB, hDIB);
CloseClipboard();
}
DeleteBitmap(hbmNew);
}
OnCommand
にこれを適応する。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_COPY:
OnCopy(hwnd);
break;
...(中略)...
}
}
「切り取り」は、コピーと削除の組み合わせである。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_CUT:
OnCopy(hwnd);
OnDelete(hwnd);
break;
case ID_COPY:
...(中略)...
}
}
このペイントには貼り付けはできないが、コピーしたものを他のペイントソフトに貼り付けできる。
ここまでのソースを次のリンクに掲載する。
モード変数により切り替えるコードは、ややこしくなりがちである。 ポリモーフィズムという考え方で、仮想関数を使ってモードの切り替えを実装しよう。
まず、paint.h
にモードのインターフェースを実装する。
struct IMode
{
IMode()
{
}
virtual ~IMode()
{
}
virtual void DoLButtonDown(HWND hwnd, POINT pt) = 0;
virtual void DoMouseMove(HWND hwnd, POINT pt) = 0;
virtual void DoLButtonUp(HWND hwnd, POINT pt) = 0;
virtual void DoPostPaint(HWND hwnd, HDC hDC) = 0;
};
そしてcanvas.cpp
のDoNormalizeRect
関数の定義の下に次のように書く。
struct ModeSelect : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
RECT rc = { s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y };
DoNormalizeRect(&rc);
DrawFocusRect(hDC, &rc);
}
};
struct ModePencil : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, pt.x, pt.y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_ptOld = pt;
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, pt.x, pt.y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
}
};
static IMode *s_pMode = new ModePencil();
void DoSetMode(MODE nMode)
{
delete s_pMode;
switch (nMode)
{
case MODE_SELECT:
s_pMode = new ModeSelect();
break;
case MODE_PENCIL:
s_pMode = new ModePencil();
break;
}
g_nMode = nMode;
}
...(中略)...
static void OnPaint(HWND hwnd)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
BitBlt(hDC, 0, 0, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY);
SelectBitmap(hMemDC, hbmOld);
s_pMode->DoPostPaint(hwnd, hDC);
DeleteDC(hMemDC);
}
EndPaint(hwnd, &ps);
}
}
...(中略)...
static void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
{
if (fDoubleClick || s_bDragging)
return;
s_bDragging = TRUE;
SetCapture(hwnd);
POINT pt = { x, y };
s_pMode->DoLButtonDown(hwnd, pt);
}
static void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
s_pMode->DoMouseMove(hwnd, pt);
}
static void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
s_pMode->DoLButtonUp(hwnd, pt);
ReleaseCapture();
s_bDragging = FALSE;
}
...(中略)...
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_SELECT:
DoSetMode(MODE_SELECT);
break;
case ID_PENCIL:
DoSetMode(MODE_PENCIL);
break;
}
}
どうだろう。少しコードが見やすくなっただろう。
これからモードを変更するときは、常にDoSetMode
関数を経由して行うものとする。
ninja
して変わりなく動くかどうか確かめてみよう。
ここまでのソースを次のリンクに掲載する。
選択した領域をドラッグできるようにしたい。まずは、
canvas.cpp
に次のような変数s_hbmFloating
とs_rcFloating
を追加する。
static HBITMAP s_hbmFloating = NULL;
static RECT s_rcFloating;
選択して移動させた領域は、浮き上げる。
そこで、次のようなDoTakeOff
とDoLanding
関数を追加する。
飛行機の「離陸」と「着陸」のようなイメージだ。
void DoTakeOff(void)
{
if (!s_hbmFloating)
{
s_hbmFloating = DoGetSubImage(g_hbm, &s_rcFloating);
DoPutSubImage(g_hbm, &s_rcFloating, NULL);
}
}
void DoLanding(void)
{
if (s_hbmFloating)
{
DoPutSubImage(g_hbm, &s_rcFloating, s_hbmFloating);
DeleteBitmap(s_hbmFloating);
s_hbmFloating = NULL;
}
SetRectEmpty(&s_rcFloating);
}
そしてModeSelect
を次のように書き換える。
struct ModeSelect : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
if (PtInRect(&s_rcFloating, pt))
{
DoTakeOff();
}
else
{
DoLanding();
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
if (s_hbmFloating)
{
OffsetRect(&s_rcFloating, pt.x - s_ptOld.x, pt.y - s_ptOld.y);
s_pt = s_ptOld = pt;
}
else
{
s_pt = pt;
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (s_hbmFloating)
{
OffsetRect(&s_rcFloating, pt.x - s_ptOld.x, pt.y - s_ptOld.y);
s_pt = s_ptOld = pt;
}
else
{
s_pt = pt;
SetRect(&s_rcFloating, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&s_rcFloating);
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
RECT rc;
if (s_hbmFloating)
rc = s_rcFloating;
else
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
if (!IsRectEmpty(&s_rcFloating) && s_hbmFloating)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, s_hbmFloating);
{
BitBlt(hDC, s_rcFloating.left, s_rcFloating.top,
s_rcFloating.right - s_rcFloating.left,
s_rcFloating.bottom - s_rcFloating.top,
hMemDC, 0, 0, SRCCOPY);
}
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
}
}
DrawFocusRect(hDC, &rc);
}
};
ninja
して実行すると、領域のドラッグが可能になる。
ここまでのソースを次のリンクに掲載する。
「すべて選択」は次のように実装する。
static void OnSelectAll(HWND hwnd)
{
DoSetMode(MODE_SELECT);
DoLanding();
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
SetRect(&s_rcFloating, 0, 0, bm.bmWidth, bm.bmHeight);
s_ptOld.x = s_rcFloating.left;
s_ptOld.y = s_rcFloating.top;
s_pt.x = s_rcFloating.right;
s_pt.y = s_rcFloating.bottom;
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_SELECT_ALL:
OnSelectAll(hwnd);
break;
}
}
ここまでのソースを次のリンクに掲載する。
「貼り付け」機能は次のように実装する。
static void OnPaste(HWND hwnd)
{
if (!IsClipboardFormatAvailable(CF_BITMAP))
return;
if (OpenClipboard(hwnd))
{
HBITMAP hbm = (HBITMAP)GetClipboardData(CF_BITMAP);
BITMAP bm;
if (GetObject(hbm, sizeof(bm), &bm))
{
DoLanding();
DoSetMode(MODE_SELECT);
SetRect(&s_rcFloating, 0, 0, bm.bmWidth, bm.bmHeight);
if (s_hbmFloating)
DeleteObject(s_hbmFloating);
s_hbmFloating = hbm;
InvalidateRect(hwnd, NULL, TRUE);
}
CloseClipboard();
}
}
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
break;
case ID_PASTE:
OnPaste(hwnd);
break;
...(中略)...
}
}
これで貼り付けが可能になった。
ここまでのソースを次のリンクに掲載する。
モードに合わせて「ツール」メニューにチェックを入れるとわかりやすい。
paint.cpp
のWM_INITMENUPOPUP
メッセージに対して、次の関数を追加する。
static void OnInitMenuPopup(HWND hwnd, HMENU hMenu, UINT item, BOOL fSystemMenu)
{
switch (g_nMode)
{
case MODE_SELECT:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_PENCIL, ID_SELECT, MF_BYCOMMAND);
break;
case MODE_PENCIL:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_PENCIL, ID_PENCIL, MF_BYCOMMAND);
break;
}
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
...(中略)...
HANDLE_MSG(hwnd, WM_INITMENUPOPUP, OnInitMenuPopup);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
これでメニュー項目にチェックが付く。
ここまでのソースを掲載する。
まずはCMakeLists.txt
。
cmake_minimum_required(VERSION 2.4)
project(paint C CXX RC)
add_definitions(-DUNICODE -D_UNICODE)
add_executable(paint WIN32 paint.cpp canvas.cpp bitmap.cpp paint_res.rc)
target_link_libraries(paint PRIVATE comctl32 comdlg32)
次はpaint.h
。
#pragma once
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <cstdio>
#include <string>
#include <strsafe.h>
#include "resource.h"
LPTSTR LoadStringDx(INT nID);
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
HBITMAP DoCreate24BppBitmap(INT cx, INT cy);
HBITMAP DoGetSubImage(HBITMAP hbm, const RECT *prc);
void DoPutSubImage(HBITMAP hbm, const RECT *prc, HBITMAP hbmSubImage);
HBITMAP LoadBitmapFromFile(LPCTSTR bmp_file);
BOOL SaveBitmapToFile(LPCTSTR bmp_file, HBITMAP hbm);
HGLOBAL DIBFromBitmap(HBITMAP hbm);
extern HBITMAP g_hbm;
enum MODE
{
MODE_SELECT,
MODE_PENCIL
};
extern MODE g_nMode;
struct IMode
{
IMode()
{
}
virtual ~IMode()
{
}
virtual void DoLButtonDown(HWND hwnd, POINT pt) = 0;
virtual void DoMouseMove(HWND hwnd, POINT pt) = 0;
virtual void DoLButtonUp(HWND hwnd, POINT pt) = 0;
virtual void DoPostPaint(HWND hwnd, HDC hDC) = 0;
};
次はbitmap.cpp
。
#include <windows.h>
#include <windowsx.h>
#define BM_MAGIC 0x4D42 /* 'BM' */
typedef struct tagKHMZ_BITMAPINFOEX
{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[256];
} KHMZ_BITMAPINFOEX, FAR *LPKHMZ_BITMAPINFOEX;
HBITMAP DoCreate24BppBitmap(INT cx, INT cy)
{
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = cx;
bmi.bmiHeader.biHeight = cy;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
HDC hDC = CreateCompatibleDC(NULL);
LPVOID pvBits;
HBITMAP ret = CreateDIBSection(hDC, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0);
DeleteDC(hDC);
return ret;
}
HBITMAP DoGetSubImage(HBITMAP hbm, const RECT *prc)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
HBITMAP ret = DoCreate24BppBitmap(cx, cy);
if (!ret)
return NULL;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, ret);
BitBlt(hMemDC2, 0, 0, cx, cy, hMemDC1, prc->left, prc->top, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
return ret;
}
void DoPutSubImage(HBITMAP hbm, const RECT *prc, HBITMAP hbmSubImage)
{
INT cx = prc->right - prc->left;
INT cy = prc->bottom - prc->top;
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbm);
if (hbmSubImage)
{
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, hbmSubImage);
BitBlt(hMemDC1, prc->left, prc->top, cx, cy, hMemDC2, 0, 0, SRCCOPY);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
}
else
{
PatBlt(hMemDC1, prc->left, prc->top, cx, cy, BLACKNESS);
}
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
}
HBITMAP LoadBitmapFromFile(LPCTSTR bmp_file)
{
HANDLE hFile;
BITMAPFILEHEADER bf;
KHMZ_BITMAPINFOEX bmi;
DWORD cb, cbImage;
LPVOID pvBits1, pvBits2;
HDC hDC;
HBITMAP hbm;
/* LoadImage can load a bottom-up bitmap */
hbm = (HBITMAP)LoadImage(NULL, bmp_file, IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE | LR_CREATEDIBSECTION);
if (hbm)
return hbm;
/* Let's load a top-down bitmap */
hFile = CreateFile(bmp_file, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
/* failed to open the file */
return NULL;
}
if (!ReadFile(hFile, &bf, sizeof(BITMAPFILEHEADER), &cb, NULL))
{
/* failed to read the file */
CloseHandle(NULL);
return NULL;
}
/* allocate the bits and read from file */
pvBits1 = NULL;
if (bf.bfType == BM_MAGIC && bf.bfReserved1 == 0 && bf.bfReserved2 == 0 &&
bf.bfSize > bf.bfOffBits && bf.bfOffBits > sizeof(BITMAPFILEHEADER) &&
bf.bfOffBits <= sizeof(BITMAPFILEHEADER) + sizeof(KHMZ_BITMAPINFOEX))
{
cbImage = bf.bfSize - bf.bfOffBits;
pvBits1 = HeapAlloc(GetProcessHeap(), 0, cbImage);
if (pvBits1)
{
if (!ReadFile(hFile, &bmi, bf.bfOffBits -
sizeof(BITMAPFILEHEADER), &cb, NULL) ||
!ReadFile(hFile, pvBits1, cbImage, &cb, NULL))
{
/* failed to read the file */
HeapFree(GetProcessHeap(), 0, pvBits1);
pvBits1 = NULL;
}
}
}
/* close the file */
CloseHandle(hFile);
if (pvBits1 == NULL)
{
return NULL; /* allocation failure */
}
/* create the DIB object and get the handle */
hbm = CreateDIBSection(NULL, (BITMAPINFO*)&bmi, DIB_RGB_COLORS,
&pvBits2, NULL, 0);
if (hbm)
{
hDC = CreateCompatibleDC(NULL);
if (!SetDIBits(hDC, hbm, 0, (UINT)labs(bmi.bmiHeader.biHeight),
pvBits1, (BITMAPINFO*)&bmi, DIB_RGB_COLORS))
{
/* failed to get the bits */
DeleteObject(hbm);
hbm = NULL;
}
DeleteDC(hDC);
}
/* clean up */
HeapFree(GetProcessHeap(), 0, pvBits1);
return hbm;
}
/* save the bitmap to a BMP/DIB file */
BOOL SaveBitmapToFile(LPCTSTR bmp_file, HBITMAP hbm)
{
BOOL fOK;
BITMAPFILEHEADER bf;
KHMZ_BITMAPINFOEX bmi;
BITMAPINFOHEADER *pbmih;
DWORD cb, cbColors;
HDC hDC;
HANDLE hFile;
LPVOID pBits;
BITMAP bm;
/* verify the bitmap */
if (!GetObject(hbm, sizeof(BITMAP), &bm))
return FALSE;
pbmih = &bmi.bmiHeader;
ZeroMemory(pbmih, sizeof(BITMAPINFOHEADER));
pbmih->biSize = sizeof(BITMAPINFOHEADER);
pbmih->biWidth = bm.bmWidth;
pbmih->biHeight = bm.bmHeight;
pbmih->biPlanes = 1;
pbmih->biBitCount = bm.bmBitsPixel;
pbmih->biCompression = BI_RGB;
pbmih->biSizeImage = (DWORD)(bm.bmWidthBytes * bm.bmHeight);
/* size of color table */
if (bm.bmBitsPixel <= 8)
cbColors = (DWORD)((1ULL << bm.bmBitsPixel) * sizeof(RGBQUAD));
else
cbColors = 0;
bf.bfType = BM_MAGIC;
bf.bfReserved1 = 0;
bf.bfReserved2 = 0;
cb = sizeof(BITMAPFILEHEADER) + pbmih->biSize + cbColors;
bf.bfOffBits = cb;
bf.bfSize = cb + pbmih->biSizeImage;
/* allocate the bits */
pBits = HeapAlloc(GetProcessHeap(), 0, pbmih->biSizeImage);
if (pBits == NULL)
return FALSE; /* allocation failure */
/* create a DC */
hDC = CreateCompatibleDC(NULL);
/* get the bits */
fOK = FALSE;
if (GetDIBits(hDC, hbm, 0, (UINT)bm.bmHeight, pBits, (BITMAPINFO *)&bmi,
DIB_RGB_COLORS))
{
/* create the file */
hFile = CreateFile(bmp_file, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_WRITE_THROUGH, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
/* write to file */
fOK = WriteFile(hFile, &bf, sizeof(bf), &cb, NULL) &&
WriteFile(hFile, &bmi, sizeof(BITMAPINFOHEADER) + cbColors, &cb, NULL) &&
WriteFile(hFile, pBits, pbmih->biSizeImage, &cb, NULL);
/* close the file */
CloseHandle(hFile);
/* if writing failed, delete the file */
if (!fOK)
{
DeleteFile(bmp_file);
}
}
}
/* delete the DC */
DeleteDC(hDC);
/* clean up */
HeapFree(GetProcessHeap(), 0, pBits);
return fOK;
}
HGLOBAL DIBFromBitmap(HBITMAP hbm)
{
BITMAP bm;
GetObject(hbm, sizeof(bm), &bm);
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = bm.bmWidth;
bmi.bmiHeader.biHeight = bm.bmHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
DWORD cbBits = bm.bmWidthBytes * bm.bmHeight;
DWORD cbSize = sizeof(BITMAPINFOHEADER) + cbBits;
HGLOBAL ret = GlobalAlloc(GHND | GMEM_SHARE, cbSize);
if (!ret)
return NULL;
LPBYTE pb = (LPBYTE)GlobalLock(ret);
CopyMemory(pb, &bmi, sizeof(BITMAPINFOHEADER));
CopyMemory(pb + sizeof(BITMAPINFOHEADER), bm.bmBits, cbBits);
GlobalUnlock(ret);
return ret;
}
次は、canvas.cpp
。
#include "paint.h"
HBITMAP g_hbm = NULL;
static BOOL s_bDragging = FALSE;
static POINT s_ptOld;
MODE g_nMode = MODE_PENCIL;
static POINT s_pt;
static HBITMAP s_hbmFloating = NULL;
static RECT s_rcFloating;
void DoNormalizeRect(RECT *prc)
{
if (prc->left > prc->right)
std::swap(prc->left, prc->right);
if (prc->top > prc->bottom)
std::swap(prc->top, prc->bottom);
}
void DoTakeOff(void)
{
if (!s_hbmFloating)
{
s_hbmFloating = DoGetSubImage(g_hbm, &s_rcFloating);
DoPutSubImage(g_hbm, &s_rcFloating, NULL);
}
}
void DoLanding(void)
{
if (s_hbmFloating)
{
DoPutSubImage(g_hbm, &s_rcFloating, s_hbmFloating);
DeleteBitmap(s_hbmFloating);
s_hbmFloating = NULL;
}
SetRectEmpty(&s_rcFloating);
}
struct ModeSelect : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
if (PtInRect(&s_rcFloating, pt))
{
DoTakeOff();
}
else
{
DoLanding();
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
if (s_hbmFloating)
{
OffsetRect(&s_rcFloating, pt.x - s_ptOld.x, pt.y - s_ptOld.y);
s_pt = s_ptOld = pt;
}
else
{
s_pt = pt;
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (s_hbmFloating)
{
OffsetRect(&s_rcFloating, pt.x - s_ptOld.x, pt.y - s_ptOld.y);
s_pt = s_ptOld = pt;
}
else
{
s_pt = pt;
SetRect(&s_rcFloating, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&s_rcFloating);
}
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
RECT rc;
if (s_hbmFloating)
rc = s_rcFloating;
else
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
if (!IsRectEmpty(&s_rcFloating) && s_hbmFloating)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, s_hbmFloating);
{
BitBlt(hDC, s_rcFloating.left, s_rcFloating.top,
s_rcFloating.right - s_rcFloating.left,
s_rcFloating.bottom - s_rcFloating.top,
hMemDC, 0, 0, SRCCOPY);
}
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
}
}
DrawFocusRect(hDC, &rc);
s_rcFloating = rc;
}
};
struct ModePencil : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, pt.x, pt.y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_ptOld = pt;
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, pt.x, pt.y);
SelectBitmap(hMemDC, hbmOld);
SelectPen(hMemDC, hPenOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
}
};
static IMode *s_pMode = new ModePencil();
void DoSetMode(MODE nMode)
{
delete s_pMode;
switch (nMode)
{
case MODE_SELECT:
s_pMode = new ModeSelect();
break;
case MODE_PENCIL:
s_pMode = new ModePencil();
break;
}
g_nMode = nMode;
}
static void OnDelete(HWND hwnd)
{
if (g_nMode != MODE_SELECT)
return;
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
DoPutSubImage(g_hbm, &rc, NULL);
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnCopy(HWND hwnd)
{
RECT rc;
if (g_nMode == MODE_SELECT)
{
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
}
else
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
SetRect(&rc, 0, 0, bm.bmWidth, bm.bmHeight);
}
HBITMAP hbmNew = DoGetSubImage(g_hbm, &rc);
if (!hbmNew)
return;
HGLOBAL hDIB = DIBFromBitmap(hbmNew);
if (OpenClipboard(hwnd))
{
EmptyClipboard();
SetClipboardData(CF_DIB, hDIB);
CloseClipboard();
}
DeleteBitmap(hbmNew);
}
static void OnSelectAll(HWND hwnd)
{
DoSetMode(MODE_SELECT);
DoLanding();
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
SetRect(&s_rcFloating, 0, 0, bm.bmWidth, bm.bmHeight);
s_ptOld.x = s_rcFloating.left;
s_ptOld.y = s_rcFloating.top;
s_pt.x = s_rcFloating.right;
s_pt.y = s_rcFloating.bottom;
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnPaste(HWND hwnd)
{
if (!IsClipboardFormatAvailable(CF_BITMAP))
return;
if (OpenClipboard(hwnd))
{
HBITMAP hbm = (HBITMAP)GetClipboardData(CF_BITMAP);
BITMAP bm;
if (GetObject(hbm, sizeof(bm), &bm))
{
DoLanding();
DoSetMode(MODE_SELECT);
SetRect(&s_rcFloating, 0, 0, bm.bmWidth, bm.bmHeight);
if (s_hbmFloating)
DeleteObject(s_hbmFloating);
s_hbmFloating = hbm;
InvalidateRect(hwnd, NULL, TRUE);
}
CloseClipboard();
}
}
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_CUT:
OnCopy(hwnd);
OnDelete(hwnd);
break;
case ID_COPY:
OnCopy(hwnd);
break;
case ID_PASTE:
OnPaste(hwnd);
break;
case ID_DELETE:
OnDelete(hwnd);
break;
case ID_SELECT_ALL:
OnSelectAll(hwnd);
break;
case ID_SELECT:
DoSetMode(MODE_SELECT);
break;
case ID_PENCIL:
DoSetMode(MODE_PENCIL);
break;
}
}
static BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
g_hbm = DoCreate24BppBitmap(320, 120);
return TRUE;
}
static void OnDestroy(HWND hwnd)
{
DeleteObject(g_hbm);
g_hbm = NULL;
}
static void OnPaint(HWND hwnd)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
BitBlt(hDC, 0, 0, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY);
SelectBitmap(hMemDC, hbmOld);
s_pMode->DoPostPaint(hwnd, hDC);
DeleteDC(hMemDC);
}
EndPaint(hwnd, &ps);
}
}
static void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
{
if (fDoubleClick || s_bDragging)
return;
s_bDragging = TRUE;
SetCapture(hwnd);
POINT pt = { x, y };
s_pMode->DoLButtonDown(hwnd, pt);
}
static void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
s_pMode->DoMouseMove(hwnd, pt);
}
static void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
s_pMode->DoLButtonUp(hwnd, pt);
ReleaseCapture();
s_bDragging = FALSE;
}
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_PAINT, OnPaint);
HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
HANDLE_MSG(hwnd, WM_MOUSEMOVE, OnMouseMove);
HANDLE_MSG(hwnd, WM_LBUTTONUP, OnLButtonUp);
case WM_CAPTURECHANGED:
s_bDragging = FALSE;
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
その次はpaint.cpp
。
#include "paint.h"
static const TCHAR s_szName[] = TEXT("My Paint");
static const TCHAR s_szCanvasName[] = TEXT("My Paint Canvas");
static HINSTANCE s_hInst = NULL;
static HWND s_hMainWnd = NULL;
static HWND s_hCanvasWnd = NULL;
LPTSTR LoadStringDx(INT nID)
{
static UINT s_index = 0;
const UINT cchBuffMax = 1024;
static TCHAR s_sz[4][cchBuffMax];
TCHAR *pszBuff = s_sz[s_index];
s_index = (s_index + 1) % _countof(s_sz);
pszBuff[0] = 0;
::LoadString(NULL, nID, pszBuff, cchBuffMax);
return pszBuff;
}
static BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
RECT rc;
GetClientRect(hwnd, &rc);
DWORD style = WS_HSCROLL | WS_VSCROLL | WS_CHILD | WS_VISIBLE;
DWORD exstyle = WS_EX_CLIENTEDGE;
HWND hCanvas = CreateWindowEx(exstyle, s_szCanvasName, s_szCanvasName, style,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hwnd, (HMENU)(INT_PTR)ctl1, s_hInst, NULL);
if (hCanvas == NULL)
return FALSE;
s_hCanvasWnd = hCanvas;
DragAcceptFiles(hwnd, TRUE);
return TRUE;
}
void OnDestroy(HWND hwnd)
{
PostQuitMessage(0);
}
void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
RECT rc;
GetClientRect(hwnd, &rc);
MoveWindow(s_hCanvasWnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
BOOL DoLoad(HWND hwnd, LPCTSTR pszFile)
{
if (HBITMAP hbm = LoadBitmapFromFile(pszFile))
{
DeleteBitmap(g_hbm);
g_hbm = hbm;
InvalidateRect(s_hCanvasWnd, NULL, TRUE);
return TRUE;
}
MessageBox(hwnd, LoadStringDx(101), NULL, MB_ICONERROR);
return FALSE;
}
static void OnOpen(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("bmp");
if (GetOpenFileName(&ofn))
{
DoLoad(hwnd, szFile);
}
}
BOOL DoSave(HWND hwnd, LPCTSTR pszFile)
{
if (SaveBitmapToFile(pszFile, g_hbm))
return TRUE;
MessageBox(hwnd, LoadStringDx(100), NULL, MB_ICONERROR);
return FALSE;
}
static void OnSave(HWND hwnd)
{
TCHAR szFile[MAX_PATH] = TEXT("");
OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
ofn.hwndOwner = hwnd;
ofn.lpstrFile = szFile;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST |
OFN_HIDEREADONLY | OFN_ENABLESIZING;
ofn.lpstrDefExt = TEXT("bmp");
if (GetSaveFileName(&ofn))
{
DoSave(hwnd, szFile);
}
}
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case ID_OPEN:
OnOpen(hwnd);
break;
case ID_SAVE:
OnSave(hwnd);
break;
case ID_EXIT:
DestroyWindow(hwnd);
break;
case ID_CUT:
case ID_COPY:
case ID_PASTE:
case ID_DELETE:
case ID_SELECT_ALL:
case ID_SELECT:
case ID_PENCIL:
SendMessage(s_hCanvasWnd, WM_COMMAND, id, 0);
break;
}
}
static void OnDropFiles(HWND hwnd, HDROP hdrop)
{
TCHAR szPath[MAX_PATH];
DragQueryFile(hdrop, 0, szPath, MAX_PATH);
DragFinish(hdrop);
DoLoad(hwnd, szPath);
}
static void OnInitMenuPopup(HWND hwnd, HMENU hMenu, UINT item, BOOL fSystemMenu)
{
switch (g_nMode)
{
case MODE_SELECT:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_PENCIL, ID_SELECT, MF_BYCOMMAND);
break;
case MODE_PENCIL:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_PENCIL, ID_PENCIL, MF_BYCOMMAND);
break;
}
}
LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_CREATE, OnCreate);
HANDLE_MSG(hwnd, WM_DESTROY, OnDestroy);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
HANDLE_MSG(hwnd, WM_DROPFILES, OnDropFiles);
HANDLE_MSG(hwnd, WM_INITMENUPOPUP, OnInitMenuPopup);
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
INT WINAPI
WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
INT nCmdShow)
{
s_hInst = hInstance;
InitCommonControls();
WNDCLASS wc;
ZeroMemory(&wc, sizeof(wc));
wc.style = 0;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wc.lpszMenuName = MAKEINTRESOURCE(1);
wc.lpszClassName = s_szName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -1;
}
ZeroMemory(&wc, sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
wc.lpfnWndProc = CanvasWndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = GetStockBrush(GRAY_BRUSH);
wc.lpszClassName = s_szCanvasName;
if (!RegisterClass(&wc))
{
MessageBoxA(NULL, "RegisterClass failed", NULL, MB_ICONERROR);
return -2;
}
s_hMainWnd = CreateWindow(s_szName, s_szName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if (!s_hMainWnd)
{
MessageBoxA(NULL, "CreateWindow failed", NULL, MB_ICONERROR);
return -3;
}
ShowWindow(s_hMainWnd, nCmdShow);
UpdateWindow(s_hMainWnd);
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(1));
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
if (TranslateAccelerator(s_hMainWnd, hAccel, &msg))
continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyAcceleratorTable(hAccel);
return 0;
}
そしてresource.h
。
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ Compatible
// This file is automatically generated by RisohEditor.
// paint_res.rc
#define ID_OPEN 100
#define ID_SAVE 101
#define ID_EXIT 102
#define ID_CUT 103
#define ID_COPY 104
#define ID_PASTE 105
#define ID_DELETE 106
#define ID_SELECT_ALL 107
#define ID_SELECT 108
#define ID_PENCIL 109
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 100
#define _APS_NEXT_COMMAND_VALUE 110
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 300
#endif
#endif
最後にリソース(paint_res.rc
)。
// paint_res.rc
// This file is automatically generated by RisohEditor.
// † <-- This dagger helps UTF-8 detection.
#include "resource.h"
#define APSTUDIO_HIDDEN_SYMBOLS
#include <windows.h>
#include <commctrl.h>
#undef APSTUDIO_HIDDEN_SYMBOLS
#pragma code_page(65001) // UTF-8
//////////////////////////////////////////////////////////////////////////////
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
//////////////////////////////////////////////////////////////////////////////
// RT_MENU
1 MENU
{
POPUP "ファイル(&F)"
{
MENUITEM "開く(&O)...\tCtrl+O", ID_OPEN
MENUITEM "名前を付けて保存(&S)...\tCtrl+S", ID_SAVE
MENUITEM SEPARATOR
MENUITEM "終了(&X)\tAlt+F4", ID_EXIT
}
POPUP "編集(&E)"
{
MENUITEM "切り取り(&T)\tCtrl+X", ID_CUT
MENUITEM "コピー(&C)\tCtrl+C", ID_COPY
MENUITEM "貼り付け(&P)\tCtrl+V", ID_PASTE
MENUITEM "削除(&D)\tDel", ID_DELETE
MENUITEM SEPARATOR
MENUITEM "すべて選択(&A)\tCtrl+A", ID_SELECT_ALL
}
POPUP "ツール(&T)"
{
MENUITEM "選択(&S)", ID_SELECT
MENUITEM "鉛筆(&P)", ID_PENCIL
}
}
//////////////////////////////////////////////////////////////////////////////
// RT_ACCELERATOR
1 ACCELERATORS
{
"O", ID_OPEN, CONTROL, VIRTKEY
"S", ID_SAVE, CONTROL, VIRTKEY
"X", ID_CUT, CONTROL, VIRTKEY
"C", ID_COPY, CONTROL, VIRTKEY
"V", ID_PASTE, CONTROL, VIRTKEY
"A", ID_SELECT_ALL, CONTROL, VIRTKEY
VK_DELETE, ID_DELETE, VIRTKEY
}
//////////////////////////////////////////////////////////////////////////////
// RT_GROUP_ICON
1 ICON "res/1041_Icon_1.ico"
//////////////////////////////////////////////////////////////////////////////
// RT_MANIFEST
#ifndef MSVC
1 24 "res/1041_Manifest_1.manifest"
#endif
//////////////////////////////////////////////////////////////////////////////
// RT_STRING
STRINGTABLE
{
100, "ファイルの保存に失敗しました。"
101, "ファイルを開くのに失敗しました。"
}
...(以下略)...
前回でお絵かきソフトの土台ができたが、まだ機能が少なく貧弱である。 さらに機能を追加しよう。
では、「四角」「丸」「直線」の3つのモードを追加してみよう。
paint.h
のMODE列挙型にMODE_RECT
、MODE_ELLIPSE
、MODE_LINE
の3つを追加する。
enum MODE
{
MODE_SELECT,
MODE_PENCIL,
MODE_RECT,
MODE_ELLIPSE,
MODE_LINE
};
extern MODE g_nMode;
リソーエディタで、リソースに
ID_RECT
→110
、
ID_ELLIPSE
→111
、
ID_LINE
→112
の3つのコマンドIDを追加し、
「ツール」メニューに「四角」「丸」「直線」メニュー項目を追加する。
1 MENU
{
POPUP "ファイル(&F)"
...(中略)...
POPUP "ツール(&T)"
{
MENUITEM "選択(&S)", ID_SELECT
MENUITEM "鉛筆(&P)", ID_PENCIL
MENUITEM SEPARATOR
MENUITEM "四角(&R)", ID_RECT
MENUITEM "丸(&E)", ID_ELLIPSE
MENUITEM "直線(&L)", ID_LINE
}
}
さらにcanvas.cpp
で、それぞれのモードを実装する。
struct ModeRect : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBRUSH hbrOld = SelectBrush(hMemDC, GetStockBrush(BLACK_BRUSH));
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
Rectangle(hMemDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hMemDC, hbrOld);
SelectPen(hMemDC, hPenOld);
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_pt = s_ptOld = pt;
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, GetStockPen(WHITE_PEN));
HBRUSH hbrOld = SelectBrush(hDC, GetStockBrush(BLACK_BRUSH));
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
Rectangle(hDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hDC, hbrOld);
SelectPen(hDC, hPenOld);
}
}
};
struct ModeEllipse : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
HBRUSH hbrOld = SelectBrush(hMemDC, GetStockBrush(BLACK_BRUSH));
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
Ellipse(hMemDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hMemDC, hbrOld);
SelectPen(hMemDC, hPenOld);
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_pt = s_ptOld = pt;
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, GetStockPen(WHITE_PEN));
HBRUSH hbrOld = SelectBrush(hDC, GetStockBrush(BLACK_BRUSH));
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
Ellipse(hDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hDC, hbrOld);
SelectPen(hDC, hPenOld);
}
}
};
struct ModeLine : IMode
{
virtual void DoLButtonDown(HWND hwnd, POINT pt)
{
s_pt = s_ptOld = pt;
}
virtual void DoMouseMove(HWND hwnd, POINT pt)
{
s_pt = pt;
InvalidateRect(hwnd, NULL, TRUE);
}
virtual void DoLButtonUp(HWND hwnd, POINT pt)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
HPEN hPenOld = SelectPen(hMemDC, GetStockPen(WHITE_PEN));
MoveToEx(hMemDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hMemDC, s_pt.x, s_pt.y);
SelectPen(hMemDC, hPenOld);
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
InvalidateRect(hwnd, NULL, TRUE);
}
s_pt = s_ptOld = pt;
}
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, GetStockPen(WHITE_PEN));
MoveToEx(hDC, s_ptOld.x, s_ptOld.y, NULL);
LineTo(hDC, s_pt.x, s_pt.y);
SelectPen(hDC, hPenOld);
}
}
};
...(中略)...
void DoSetMode(MODE nMode)
{
delete s_pMode;
switch (nMode)
{
case MODE_SELECT:
s_pMode = new ModeSelect();
break;
case MODE_PENCIL:
s_pMode = new ModePencil();
break;
case MODE_RECT:
s_pMode = new ModeRect();
break;
case MODE_ELLIPSE:
s_pMode = new ModeEllipse();
break;
case MODE_LINE:
s_pMode = new ModeLine();
break;
}
g_nMode = nMode;
}
...(中略)...
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_PENCIL:
DoSetMode(MODE_PENCIL);
break;
case ID_RECT:
DoSetMode(MODE_RECT);
break;
case ID_ELLIPSE:
DoSetMode(MODE_ELLIPSE);
break;
case ID_LINE:
DoSetMode(MODE_LINE);
break;
}
}
さらにpaint.cpp
を新しいモードに順応させる。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_SELECT:
case ID_PENCIL:
case ID_RECT:
case ID_ELLIPSE:
case ID_LINE:
SendMessage(s_hCanvasWnd, WM_COMMAND, id, 0);
break;
}
}
...(中略)...
void OnInitMenuPopup(HWND hwnd, HMENU hMenu, UINT item, BOOL fSystemMenu)
{
switch (g_nMode)
switch (g_nMode)
{
case MODE_SELECT:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_LINE, ID_SELECT, MF_BYCOMMAND);
break;
case MODE_PENCIL:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_LINE, ID_PENCIL, MF_BYCOMMAND);
break;
case MODE_RECT:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_LINE, ID_RECT, MF_BYCOMMAND);
break;
case MODE_ELLIPSE:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_LINE, ID_ELLIPSE, MF_BYCOMMAND);
break;
case MODE_LINE:
CheckMenuRadioItem(hMenu, ID_SELECT, ID_LINE, ID_LINE, MF_BYCOMMAND);
break;
}
}
これで3つのモードを実装できた。
ここまでのソースは次のリンクに掲載する。
線の色と、塗りつぶしの色を変更できるようにしたい。
リソーエディタでコマンドIDの
ID_LINE_COLOR
→113
、
ID_FILL_COLOR
→114
を追加する。
「ツール」メニューに「線の色(&C)...」「塗りつぶしの色(&F)...」メニュー項目を追加する。
1 MENU
{
...(中略)...
POPUP "ツール(&T)"
{
MENUITEM "選択(&S)", ID_SELECT
MENUITEM "鉛筆(&P)", ID_PENCIL
MENUITEM SEPARATOR
MENUITEM "四角(&R)", ID_RECT
MENUITEM "丸(&E)", ID_ELLIPSE
MENUITEM "直線(&L)", ID_LINE
MENUITEM SEPARATOR
MENUITEM "線の色(&C)...", ID_LINE_COLOR
MENUITEM "塗りつぶしの色(&F)...", ID_FILL_COLOR
}
}
paint.cpp
を新しいコマンドに順応させる。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_SELECT:
case ID_PENCIL:
case ID_RECT:
case ID_ELLIPSE:
case ID_LINE:
case ID_LINE_COLOR:
case ID_FILL_COLOR:
SendMessage(s_hCanvasWnd, WM_COMMAND, id, 0);
break;
}
}
canvas.cpp
に、ペンとブラシと色の変数を追加する。
static COLORREF s_rgbLine = RGB(255, 255, 255);
static COLORREF s_rgbFill = RGB(0, 0, 0);
static HPEN s_hPen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
static HBRUSH s_hBrush = CreateSolidBrush(RGB(0, 0, 0));
ユーザに色を選択させる関数を追加する。
void DoChooseColor(HWND hwnd, BOOL bFill)
{
static COLORREF s_custom_colors[16];
CHOOSECOLOR cc = { sizeof(cc) };
cc.hwndOwner = hwnd;
if (bFill)
cc.rgbResult = s_rgbFill;
else
cc.rgbResult = s_rgbLine;
cc.lpCustColors = s_custom_colors;
cc.Flags = CC_FULLOPEN | CC_RGBINIT;
if (ChooseColor(&cc))
{
if (bFill)
{
s_rgbFill = cc.rgbResult;
DeleteObject(s_hBrush);
s_hBrush = CreateSolidBrush(s_rgbFill);
}
else
{
s_rgbLine = cc.rgbResult;
DeleteObject(s_hPen);
s_hPen = CreatePen(PS_SOLID, 1, s_rgbLine);
}
}
}
WM_DESTROY
メッセージが来たらペンとブラシを破棄する。
static void OnDestroy(HWND hwnd)
{
DeleteObject(g_hbm);
g_hbm = NULL;
DeleteObject(s_hPen);
s_hPen = NULL;
DeleteObject(s_hBrush);
s_hBrush = NULL;
}
コマンドを実装する。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
break;
case ID_LINE_COLOR:
DoChooseColor(hwnd, FALSE);
break;
case ID_FILL_COLOR:
DoChooseColor(hwnd, TRUE);
break;
}
}
そして、線を引くときと、塗りつぶすときに、これらのペンとブラシを使用するようにする。
GetStockPen(WHITE_PEN)
をs_hPen
で置換する。
GetStockBrush(BLACK_BRUSH)
をs_hBrush
で置換する。
これで色が指定できるようになった。
ここまでのソースは次のリンクに掲載する。
キャンバスのサイズを変更できるようにしたい。
リソーエディタでコマンドIDのID_CANVAS_SIZE
→115
と、
リソースIDのIDD_SIZE
→100
を追加し、
「ツール」メニューにメニュー項目「キャンバス サイズ(&V)...」を追加する。
1 MENU
{
...(中略)...
POPUP "ツール(&T)"
{
...(中略)...
MENUITEM "塗りつぶしの色(&F)...", ID_FILL_COLOR
MENUITEM SEPARATOR
MENUITEM "キャンバス サイズ(&V)...", ID_CANVAS_SIZE
}
}
リソーエディタで次のようなダイアログIDD_SIZE
を追加する。
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
IDD_SIZE DIALOG 0, 0, 163, 80
CAPTION "キャンバスのサイズ変更"
STYLE DS_CENTER | DS_MODALFRAME | WS_POPUPWINDOW | WS_CAPTION
FONT 9, "MS UI Gothic"
{
LTEXT "幅(&W):", -1, 7, 9, 33, 13
EDITTEXT edt1, 46, 8, 60, 14
LTEXT "ピクセル", -1, 116, 9, 32, 12
LTEXT "高さ(&H):", -1, 7, 37, 32, 11
EDITTEXT edt2, 46, 35, 60, 14
LTEXT "ピクセル", -1, 115, 37, 32, 12
DEFPUSHBUTTON "OK", IDOK, 7, 54, 60, 14
PUSHBUTTON "キャンセル", IDCANCEL, 78, 55, 60, 14
}
paint.cpp
のOnCommand
を、新しいコマンドIDのID_CANVAS_SIZE
に順応させる。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_LINE_COLOR:
case ID_FILL_COLOR:
case ID_CANVAS_SIZE:
SendMessage(s_hCanvasWnd, WM_COMMAND, id, 0);
break;
}
}
canvas.cpp
のOnCommand
に新しいコマンドIDのID_CANVAS_SIZE
を追加する。
static void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
...(中略)...
case ID_FILL_COLOR:
DoChooseColor(hwnd, TRUE);
break;
case ID_CANVAS_SIZE:
OnCanvasSize(hwnd);
break;
}
canvas.cpp
に次のようなコードを追記する。
BOOL Size_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
SetDlgItemInt(hwnd, edt1, bm.bmWidth, FALSE);
SetDlgItemInt(hwnd, edt2, bm.bmHeight, FALSE);
return TRUE;
}
void Size_OnOK(HWND hwnd)
{
BOOL bTrans;
INT cx = GetDlgItemInt(hwnd, edt1, &bTrans, FALSE);
if (!bTrans)
{
MessageBox(hwnd, TEXT("ERROR"), NULL, MB_ICONERROR);
return;
}
INT cy = GetDlgItemInt(hwnd, edt1, &bTrans, FALSE);
if (!bTrans)
{
MessageBox(hwnd, TEXT("ERROR"), NULL, MB_ICONERROR);
return;
}
HBITMAP hbm = DoCreate24BppBitmap(cx, cy);
RECT rc;
SetRect(&rc, 0, 0, cx, cy);
DoPutSubImage(hbm, &rc, g_hbm);
DeleteObject(g_hbm);
g_hbm = hbm;
EndDialog(hwnd, IDOK);
}
void Size_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDOK:
Size_OnOK(hwnd);
break;
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
INT_PTR CALLBACK
SizeDialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hwnd, WM_INITDIALOG, Size_OnInitDialog);
HANDLE_MSG(hwnd, WM_COMMAND, Size_OnCommand);
}
return 0;
}
static void OnCanvasSize(HWND hwnd)
{
if (DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_SIZE),
hwnd, SizeDialogProc) == IDOK)
{
InvalidateRect(hwnd, NULL, TRUE);
}
}
これでキャンバスサイズを変更できるようになった。
ここまでのソースは次のリンクに掲載する。
例えば、キャンバスのサイズが1000×1000ピクセルのような大きなサイズのとき、キャンバスはメインウィンドウをはみ出るかもしれない。 画像がウィンドウよりも大きいときでも、スクロール(画面の中身をずらす)によって画像全体を閲覧できるようにしたい。
まず、canvas.cpp
にDoUpdateCanvas
という関数を追加する。
void DoUpdateCanvas(HWND hwnd, HBITMAP hbm, BOOL bResetPos = FALSE)
{
if (g_hbm != hbm)
{
DeleteObject(g_hbm);
g_hbm = hbm;
}
BITMAP bm;
GetObject(hbm, sizeof(bm), &bm);
RECT rc;
GetClientRect(hwnd, &rc);
{
SCROLLINFO si = { sizeof(si) };
si.fMask = SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
si.nMin = 0;
si.nMax = bm.bmWidth;
si.nPage = rc.right - rc.left;
if (bResetPos)
{
si.fMask |= SIF_POS;
si.nPos = 0;
}
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
}
{
SCROLLINFO si = { sizeof(si) };
si.fMask = SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
si.nMin = 0;
si.nMax = bm.bmHeight;
si.nPage = rc.bottom - rc.top;
if (bResetPos)
{
si.fMask |= SIF_POS;
si.nPos = 0;
}
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
}
}
この関数は、キャンバスのスクロール情報を更新する。 キャンバスのビットマップハンドルやサイズを更新するときに、この関数を呼ぶことにする。
次にWM_HSCROLL
、WM_VSCROLL
、WM_MOUSEWHEEL
、WM_SIZE
メッセージの処理を実装する。
static void OnHScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
{
SCROLLINFO si = { sizeof(si) };
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_HORZ, &si);
INT nOldPos = si.nPos;
INT nNewPos = nOldPos;
switch (code)
{
case SB_LINELEFT:
--nNewPos;
break;
case SB_LINERIGHT:
++nNewPos;
break;
case SB_PAGELEFT:
nNewPos -= si.nPage;
break;
case SB_PAGERIGHT:
nNewPos += si.nPage;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
nNewPos = pos;
break;
}
if (nNewPos < si.nMin)
nNewPos = si.nMin;
if (nNewPos > si.nMax - si.nPage)
nNewPos = si.nMax - si.nPage;
si.fMask = SIF_POS;
si.nPos = nNewPos;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnVScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
{
SCROLLINFO si = { sizeof(si) };
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_VERT, &si);
INT nOldPos = si.nPos;
INT nNewPos = nOldPos;
switch (code)
{
case SB_LINEUP:
--nNewPos;
break;
case SB_LINEDOWN:
++nNewPos;
break;
case SB_PAGEUP:
nNewPos -= si.nPage;
break;
case SB_PAGEDOWN:
nNewPos += si.nPage;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
nNewPos = pos;
break;
}
if (nNewPos < si.nMin)
nNewPos = si.nMin;
if (nNewPos > si.nMax - si.nPage)
nNewPos = si.nMax - si.nPage;
si.fMask = SIF_POS;
si.nPos = nNewPos;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnMouseWheel(HWND hwnd, int xPos, int yPos, int zDelta, UINT fwKeys)
{
INT x = GetScrollPos(hwnd, SB_HORZ);
INT y = GetScrollPos(hwnd, SB_VERT);
if (fwKeys & MK_SHIFT)
x += -zDelta;
else
y += -zDelta;
SetScrollPos(hwnd, SB_HORZ, x, TRUE);
SetScrollPos(hwnd, SB_VERT, y, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
}
static void OnSize(HWND hwnd, UINT state, int cx, int cy)
{
DoUpdateCanvas(hwnd, g_hbm, FALSE);
}
LRESULT CALLBACK
CanvasWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
...(中略)...
HANDLE_MSG(hwnd, WM_LBUTTONUP, OnLButtonUp);
HANDLE_MSG(hwnd, WM_HSCROLL, OnHScroll);
HANDLE_MSG(hwnd, WM_VSCROLL, OnVScroll);
HANDLE_MSG(hwnd, WM_MOUSEWHEEL, OnMouseWheel);
HANDLE_MSG(hwnd, WM_SIZE, OnSize);
case WM_CAPTURECHANGED:
s_bDragging = FALSE;
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
これでキャンバスのサイズが変更されたときやスクロールが起きたときに、適切に処理ができる。
次に、マウスメッセージが起きたとき、少し位置をずらさなければいけない。
static void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
{
if (fDoubleClick || s_bDragging)
return;
s_bDragging = TRUE;
SetCapture(hwnd);
POINT pt = { x, y };
pt.x += GetScrollPos(hwnd, SB_HORZ);
pt.y += GetScrollPos(hwnd, SB_VERT);
s_pMode->DoLButtonDown(hwnd, pt);
}
static void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
pt.x += GetScrollPos(hwnd, SB_HORZ);
pt.y += GetScrollPos(hwnd, SB_VERT);
s_pMode->DoMouseMove(hwnd, pt);
}
static void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
{
if (!s_bDragging)
return;
POINT pt = { x, y };
pt.x += GetScrollPos(hwnd, SB_HORZ);
pt.y += GetScrollPos(hwnd, SB_VERT);
s_pMode->DoLButtonUp(hwnd, pt);
ReleaseCapture();
s_bDragging = FALSE;
}
WM_PAINT
の処理でも、描画を少しずらす必要がある。
static void OnPaint(HWND hwnd)
{
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, g_hbm);
INT x = -GetScrollPos(hwnd, SB_HORZ);
INT y = -GetScrollPos(hwnd, SB_VERT);
BitBlt(hDC, x, y, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY);
SelectBitmap(hMemDC, hbmOld);
s_pMode->DoPostPaint(hwnd, hDC);
DeleteDC(hMemDC);
}
EndPaint(hwnd, &ps);
}
}
利便のために、WM_CREATE
の処理でキャンバスのウィンドウハンドルを保持しておく。
キャンバスを大きめ(500×500ピクセル)に設定する。
static HWND s_hCanvasWnd = NULL;
...(中略)...
static BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
s_hCanvasWnd = hwnd;
DoUpdateCanvas(hwnd, DoCreate24BppBitmap(500, 500), TRUE);
return TRUE;
}
保存したウィンドウハンドルをSize_OnOK
で次のように使用する。
void Size_OnOK(HWND hwnd)
{
BOOL bTrans;
...(中略)...
SetRect(&rc, 0, 0, cx, cy);
DoPutSubImage(hbm, &rc, g_hbm);
DoUpdateCanvas(s_hCanvasWnd, hbm, TRUE);
EndDialog(hwnd, IDOK);
}
スクロール位置でずらすために、
ModeRect::DoPostPaint
に修正が必要だ。
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, s_hPen);
HBRUSH hbrOld = SelectBrush(hDC, s_hBrush);
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
INT x = GetScrollPos(hwnd, SB_HORZ);
INT y = GetScrollPos(hwnd, SB_VERT);
OffsetRect(&rc, -x, -y);
DoNormalizeRect(&rc);
Rectangle(hDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hDC, hbrOld);
SelectPen(hDC, hPenOld);
}
}
ModeEllipse::DoPostPaint
にも修正が必要だ。
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, s_hPen);
HBRUSH hbrOld = SelectBrush(hDC, s_hBrush);
RECT rc;
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
DoNormalizeRect(&rc);
INT x = GetScrollPos(hwnd, SB_HORZ);
INT y = GetScrollPos(hwnd, SB_VERT);
OffsetRect(&rc, -x, -y);
Ellipse(hDC, rc.left, rc.top, rc.right, rc.bottom);
SelectBrush(hDC, hbrOld);
SelectPen(hDC, hPenOld);
}
}
また、ModeLine::DoPostPaint
にも修正が必要だ。
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
if (memcmp(&s_pt, &s_ptOld, sizeof(POINT)) != 0)
{
HPEN hPenOld = SelectPen(hDC, s_hPen);
RECT rc;
POINT pt1 = { s_ptOld.x, s_ptOld.y};
POINT pt2 = { s_pt.x, s_pt.y };
INT x = GetScrollPos(hwnd, SB_HORZ);
INT y = GetScrollPos(hwnd, SB_VERT);
pt1.x += -x;
pt2.x += -x;
pt1.y += -y;
pt2.y += -y;
MoveToEx(hDC, pt1.x, pt1.y, NULL);
LineTo(hDC, pt2.x, pt2.y);
SelectPen(hDC, hPenOld);
}
}
ModeSelect::DoPostPaint
にも修正が必要だ。
virtual void DoPostPaint(HWND hwnd, HDC hDC)
{
RECT rc;
INT x = GetScrollPos(hwnd, SB_HORZ);
INT y = GetScrollPos(hwnd, SB_VERT);
if (s_hbmFloating)
{
rc = s_rcFloating;
}
else
{
SetRect(&rc, s_ptOld.x, s_ptOld.y, s_pt.x, s_pt.y);
}
DoNormalizeRect(&rc);
if (!IsRectEmpty(&s_rcFloating) && s_hbmFloating)
{
if (HDC hMemDC = CreateCompatibleDC(NULL))
{
HBITMAP hbmOld = SelectBitmap(hMemDC, s_hbmFloating);
{
BitBlt(hDC, s_rcFloating.left - x, s_rcFloating.top - y,
s_rcFloating.right - s_rcFloating.left,
s_rcFloating.bottom - s_rcFloating.top,
hMemDC, 0, 0, SRCCOPY);
}
SelectBitmap(hMemDC, hbmOld);
DeleteDC(hMemDC);
}
}
OffsetRect(&rc, -x, -y);
DrawFocusRect(hDC, &rc);
}
これでスクロールはバッチリだ。
ここまでのソースは次のリンクに掲載する。
このペイントで鉛筆で線を書いているとき、どうも画面がちらつく。
標準のウィンドウの描画方法では、
WM_ERASEBKGND
メッセージの処理でhbrBackground
の背景ブラシで背景を塗りつぶし、
その上をWM_PAINT
メッセージの処理で描画するようになっている。
ここで、WM_ERASEBKGND
をスキップしてWM_PAINT
で「ダブルバッファリング」すれば、
ちらつきをなくすことができる。ダブルバッファリングとは、ビットマップをバッファとして使って
描画途中の状態を表示せずに、最後にバッファを使って一気に描画することを意味する。
まず、paint.cpp
のキャンバスウィンドウクラスのRegisterClass
でhbrBackground
にNULLを指定する。
これでWM_ERASEBKGND
をスキップすることができる。
次に、canvas.cpp
のOnPaint
を次のように書き換える。
static void OnPaint(HWND hwnd)
{
RECT rc;
GetClientRect(hwnd, &rc);
INT cx = rc.right - rc.left, cy = rc.bottom - rc.top;
BITMAP bm;
GetObject(g_hbm, sizeof(bm), &bm);
PAINTSTRUCT ps;
if (HDC hDC = BeginPaint(hwnd, &ps))
{
if (HBITMAP hbmBuffer = CreateCompatibleBitmap(hDC, cx, cy))
{
if (HDC hMemDC1 = CreateCompatibleDC(NULL))
{
HBITMAP hbm1Old = SelectBitmap(hMemDC1, hbmBuffer);
FillRect(hMemDC1, &rc, GetStockBrush(GRAY_BRUSH));
if (HDC hMemDC2 = CreateCompatibleDC(NULL))
{
HBITMAP hbm2Old = SelectBitmap(hMemDC2, g_hbm);
INT x = -GetScrollPos(hwnd, SB_HORZ);
INT y = -GetScrollPos(hwnd, SB_VERT);
BitBlt(hMemDC1, x, y, bm.bmWidth, bm.bmHeight,
hMemDC2, 0, 0, SRCCOPY);
s_pMode->DoPostPaint(hwnd, hMemDC1);
SelectBitmap(hMemDC2, hbm2Old);
DeleteDC(hMemDC2);
}
BitBlt(hDC, 0, 0, cx, cy, hMemDC1, 0, 0, SRCCOPY);
SelectBitmap(hMemDC1, hbm1Old);
DeleteDC(hMemDC1);
}
DeleteObject(hbmBuffer);
}
EndPaint(hwnd, &ps);
}
}
これでちらつきなしにキャンバスが描画される。
ここまでのソースは次のリンクに掲載する。
ここまでC++/Win32のプログラミングについて解説した。少しずつ修正・改良・テストするという手法なら、くじけず開発を続けることができる。GitHubやインターネットにはもっと多くの情報が掲載されている。研究を続け、どんどん工夫していけば、高度なアプリも作れるようになれるだろう。
この文書では解説できなかったが、GitとGitHubの使い方やReactOSの開発についてもどこかで紹介したいと考えている。
この文書を出版するにあたって、文書化にPandocとmarkdownが非常に役立った。この場をもって感謝したい。