Skip to content

katahiromz/win32-dev-ja

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

はじめに

この文書について

スマホに親しんだ若者がWindows 10で現代的な C++/Win32 のプログラムを作る場合に役立てばいいなと思って作りました。

基本精神

文字がわからなければ、親か先生に「あいうえお」と「アルファベット」を教えてもらって下さい。

パソコンの使い方が分からなければ、パソコン関係の本を調べる。Windowsを知らなければWindowsの本を調べる。

単語がわからなかったらGoogleで調べる。

漢字がわからなかったら漢和辞典を見る。

無関係なことや細かいことは飛ばしてもよい。

調べても調べてもわからなかったらひとまず、ほっといて先読みしてみる。

テキストファイルの作り方

最初に高機能なテキストエディタ(サクラエディタ、秀丸、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:\WindowsC:\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キー(Ctrlキーを押しながらFキー)で検索を開始できる。

Grep

複数のテキストファイルからある文字列を探すには、サクラエディタでgrep(グレップ)機能を使う(「検索」メニュー→「Grep...」)。

ファイルのロックについて

Windowsでは、アプリでファイルを開くと、アプリはそのファイルが変更されないようにロックすることができる。ロックされたファイルは開いたり、変更できない。ファイルが開けない場合は、すでに開いているプログラムがないか確認しよう。

プログラムとプロセスについて

EXEファイルとDLLファイル

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がついてくる

という利点がある。

RosBEのインストール

では早速、Windows版のReactOS Build Environment (RosBE) をインストールしてみよう。次のリンクからWindows版のRosBEを選択すれば、ダウンロードできる。

ダウンロードに成功したらインストールしよう。ファイルアイコンをダブルクリックすれば、インストールが始まる。

RosBE サイト\

RosBEの起動

インストールに成功したら、デスクトップにRosBEのアイコンが出来ているはずである。ダブルクリックしてRosBEを起動しよう。

RosBE アイコン\

起動すると次のようなRosBEのコマンドプロンプトが表示される。

RosBE コマンドプロンプト\

著者のユーザ名はkatahiromzなので、読者の環境ではいくつか表示が異なるかもしれない。

コマンドプロンプトの練習

コマンドプロンプトは、コマンドを受けとって、何かの処理を行う対話型端末である。1コマンドを入力し、Enterキーを押すと、コマンドプロンプトでコマンドを実行できる。

以下では、コマンドプロンプトの使い方を少し解説する。

CDコマンド

CDコマンドはディレクトリ(フォルダの位置)を移動するコマンドである。

CDコマンドの実行例\

cd ..で一つ上のフォルダに移動できる。cd (フォルダパス)で現在のディレクトリを移動できる。相対パスと絶対パスのいずれかを指定できる。

試しに、マイドキュメントに移動してみよう。マイドキュメントの位置(例えば、筆者の環境ではC:\User\katahiromz\Documents)を確認して、CDコマンドで指定すると移動できる。

CDコマンドの実行例2\

DIRコマンド

DIRコマンドで現在のフォルダにあるファイルやフォルダの一覧を見ることができる。CDコマンドでC:\Windows\system32に移動してdirを実行してみよう。

コマンドを中断するには

C:\Windows\system32には、大量のファイルがあるので、ここでDIRコマンドを実行すると、実行が止まらなくなるかもしれない。Ctrl+Cを押すと、現在実行中のコマンドを中断することができる。

DELコマンド

DELコマンドでファイルを削除できる。マイドキュメントに移動して、DEL "(重要なファイルの名前)"を実行して、重要なファイルを消してみよう。貴重な記録を消すことができる。

MD/MKDIRコマンド

MDコマンド(MKDIRコマンド)でフォルダを作成できる。

C:ドライブのルートに「dev」というフォルダを作り、その中に「cxx」というフォルダを作ってみよう。

cd C:\
md dev
cd dev
md cxx
dir

このようにコマンドを入力すると以下のような表示になる。

MDコマンドの実行例\

START .コマンド

start .(スタート、スペース、ドット)コマンドを使えば現在のフォルダをエクスプローラーで開くことができる。

C++/Win32開発の実践

初めてのC++プログラム(hello)

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");
}

念のため、プログラミングで使う記号の打ち方と名前について確認しよう。以下は半角英数モードで入力する。

  1. #include#はシャープ記号で、キーボードのShiftキーを押しながらキーボード左上の「3」を押せば入力できる。
  2. <cstdio><>は、不等号であり、Shiftキーを押しながらキーボードの「」、もしくは「」を押せば入力できる。
  3. ()は、丸カッコであり、Shiftキーを押しながらキーボード中央の「8」または「9」を押せば入力できる。
  4. "は、二重引用符であり、Shiftキーを押しながらキーボード左上の「2」を押せば入力できる。
  5. \は、バックスラッシュ(\)または半角の円記号(¥)であり、半角英数モードでキーボード右下の「」を押せば入力できる。
  6. ;は、セミコロンである。半角英数モードでキーボードの「」を押せば入力できる。

printfの呼び出しが右にずれているのは、インデント(indent; 字下げ)といって、プログラムの構造を見やすくするためである。キーボードの左側のTabキーを押せばインデントできる。

コンパイルの方法

RosBEではC/C++コンパイラとしてgcc/g++を使用する。CDコマンドでC:\dev\cxxに移動し、次のように入力すればhello.cppをコンパイル・実行できる。

g++ hello.cpp -o hello
hello

エラーメッセージが表示されたら、何か文字を間違えているのでしょう。hello.cppの内容を注意深く確認して下さい。

RosBEでC++コンパイル\

無事にコンパイルが終了すると、EXEファイルC:\dev\cxx\hello.exeが作成され、hello.exeが実行可能になる。helloと入力すると、同じフォルダにあるhello.exeが実行され、Hello, worldと表示される。

このようにしてRosBEで作成したプログラムは、おそらくWindows XPでも実行可能である。

これで、あなたはWindows XP以降で動作するC++プログラムを作成できた。

初めてのWin32プログラム(hello2)

次は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の実行イメージ\

コマンドプロンプトからhello2.exeを実行できるし、hello2.exeをダブルクリックしても実行可能である。

これであなたもWin32プログラムを作り、Win32 API関数を呼び出すことでWin32 APIの入口に入ったのである。「見習いWin32プログラマ」の称号を授けよう。

CMake/Ninjaとは

ここまで、コマンドプロンプトでいちいちコマンドを入力してビルドをしていたが、複雑なプロジェクトになると、コマンド入力では対応できない。C/C++ではCMakeというビルド支援ソフトを使うと、ビルド処理を自動化できる。

CMakeでは次のような手順でプロジェクトをビルドする。

  1. プロジェクトのフォルダにCMakeLists.txtというテキストファイルを作成する。CMakeLists.txtには、プロジェクトのビルド方法をCMakeの言葉で記述する。
  2. CMakeプログラムを起動してCMakeLists.txtからビルドに必要なファイルを生成させる。
  3. 生成されたファイルを使ってプロジェクトをビルドする。

RosBEでは実際にビルドを実行させるのはNinjaという名のジェネレータ(generator)である。MinGWやMSYS2ではMakefile であり、Visual Studioではプロジェクトファイル&ソリューションファイルである。cmake-Gオプションでジェネレータを指定できる。ジェネレータの一覧はcmake -Gコマンドで見ることができる。

CMake/Ninjaによるビルド

それではCMake/Ninjaによるビルドを試してみよう。まずは、プロジェクトに必要なファイルをフォルダで分けないといけない。C:\dev\cxxhello2というフォルダを作成し、そこに先ほど作成した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

実行結果は次の通り。

CMake/Ninjaによるビルド\

ビルドの際にEXE以外にさまざまなファイルが作成されるが、無視してもよい。

リソーエディタのインストール

次は、ダイアログ ボックス(dialog box; 通称ダイアログ)を表示するプログラムを作成してみよう。ダイアログとはユーザーに対して対話的なウィンドウのことであり、メッセージボックスもダイアログの一種である。

しかし、簡単にダイアログを作成するには「リソースファイル」(拡張子.rc)というものが必要になる。そこで、リソースを編集・作成するための「リソースエディタ」というものが必要になる。

この文書ではリソースエディタとして「リソーエディタ」(RisohEditor)を使用する。次のリンクからリソーエディタをダウンロード&インストールして下さい。

リソーエディタをインストールすれば、次のようなアイコンがデスクトップに作成される。

リソーエディタ\

リソーエディタの詳しい使い方については、次のまとめサイトを参照されたい。

初めてのダイアログアプリ(dialog)

ではダイアログアプリを作成しよう。C:\dev\cxxdialogというフォルダを作成し、次の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は、ウィンドウのハンドルを格納する型である。UINTunsigned int型と同じである。WPARAMLPARAMは、ポインタと同じサイズの整数型である。DialogProc関数ではWM_INITDIALOGメッセージとWM_COMMANDメッセージを処理している。DialogBoxWM_INITDIALOGなどの意味については、それをインターネットで検索すれば出てくる。MAKEINTRESOURCEマクロは、整数のリソース名を指定するのに使う。

開発が進むにつれて、複雑なコードを何度も入力するはめになるが、WinMainDialogProcOnInitDialogなどのよく使うコードは、コピーしたり、テンプレートを使ったり、マクロなどで自動入力すれば問題ない。入力補助としてMsgCrackというソフトがあるので活用されたい。

次はリソースファイルである。リソーエディタで以下の手順に従ってdialog_res.rcファイルを作成する。dialog_resの下線(_)は、Shiftキーを押しながら「」で入力する。名前に_resを付けたのはオブジェクトファイルの名前を衝突させないためである。

  1. リソーエディタを開く。
  2. 「編集」メニューから「追加」→「ダイアログを追加」を選び、「リソースの名前」に「1」(いち)を入力し、「OK」ボタンを押す。RT_DIALOG1日本語が追加される。
  3. 「ファイル」メニューから「名前を付けて保存」を選び、c:\dev\cxx\dialogに「dialog_res.rc」という名前で保存する。
  4. 「保存」オプションが表示されたら、「言語別にファイルを分ける」のチェックを外し、「OK」ボタンを押す。
  5. 左下に「ファイルを保存しました」と表示されたら完了。リソーエディタを閉じる。

このように保存すると、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がビルドされる。

dialogのビルド\

一行ずつ解説しよう。CMakeLists.txtadd_executableWIN32があるのは、main関数を使わず、WinMain関数を使うためである。WinMain関数を使えば起動時に黒い画面は表示されない。 リソースをコンパイルするために、dialog_res.rcを追加した。

target_link_librariesについてはdialog.exeにリンクするDLLファイルcomctl32を指定している。このcomctl32については後述する。 よく使うDLLファイルのkernel32user32については明示的にリンクしなくても勝手にリンクされる。

それではdialog.exeを実行してみよう。次のような何の変哲もないダイアログが開かれるはずである。

dialog.exeのイメージ\

「OK」や「キャンセル」を押したらEndDialog関数でダイアログを終了するだけだ。EndDialogがなければ終了しないダイアログアプリになる。

「OK」ボタンの処理を追加する

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"というファイルを作成するようになった。

ダイアログアプリにアイコンを追加する

このダイアログアプリにメインアイコンを追加しよう。アイコンを追加すれば、アプリのアイコンを変更することができる。

  1. インターネットから「無料素材」のアイコンファイル (拡張子.ico)を探してダウンロードする。
  2. リソーエディタでdialog_res.rcを開く。
  3. 「編集」メニューから「追加」→「アイコンを追加」を順番に選ぶ。
  4. 「参照」ボタンを押してアイコンファイルを指定する。
  5. リソースの名前に「1」(いち)と入力する
  6. 「OK」ボタンを押すと、RT_GROUP_ICONRT_ICONが追加される。
  7. 上書き保存する。

アイコンを追加\

さらにダイアログでこのアイコンを使うように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_hIcons_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_DIALOG1日本語を選択し、ダブルクリックする。「ダイアログの編集」が開かれる。

ダイアログの編集\

ラベルを追加する。右クリックして「コントロールの追加」を選ぶ。

ラベルの追加\

「定義済みControl:」にLTEXTと入力する。「キャプション:」に「整数:」と入力する。「ID:」に「stc1」と入力する。「OK」ボタンを押す。

ラベル「整数:」が追加されるのでサイズと位置を調整する。

ラベルの追加2\

次にテキストボックスを追加する。右クリックして「コントロールの追加」を選ぶ。

テキストボックスの追加\

「定義済みControl:」に「EDITTEXT」と入力し、「ID:」に「edt1」と入力する。「OK」ボタンを押す。

ボタンが追加される。位置とサイズを調整する。

テキストボックスの追加2\

「ダイアログの編集」を閉じ、変更内容を上書き保存する。

これでラベル(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に対応しているところとバッファサイズを指定できるところが違う。 バッファサイズの指定により、バッファオーバーフローを回避できる。 StringCbPrintfWCbとは、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\

1回Tabキーを押す。

フォーカス2\

2回目。

フォーカス3\

見ればわかるように、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を実行してもう一度試してみよう。

フォーカス4\

今度は、最初に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"二倍"と書くのは、ソースコード互換性がない。

そこでリソースの「文字列テーブル」というものを使う。

  1. リソーエディタでdialog_res.rcを開く。
  2. 「編集」メニューの「追加」→「文字列テーブルを追加」を選ぶ。
  3. 「OK」ボタンを押す。文字列テーブル「RT_STRING」→「日本語」が追加される。
  4. 次のように編集する。
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.dllInitCommonControls関数の呼び出しを追加する。

  1. リソーエディタでdialog_res.rcを開く。
  2. 「編集」メニューから「追加」→「マニフェストを追加」を選ぶ。
  3. 「リソースの名前」に「1」を入力して、「OK」ボタンを押す。
  4. 上書き保存する。

これでマニフェストを追加できた。次は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を追加すれば、見た目がカッコ良くなる。わかったかな?

Unicode版のAPIを優先する

一部のAPI関数はANSI版とUnicode版に分かれている。CMakeではA/Wのいずれも指定しないときは、ANSI版が使われる。Unicode版を優先したい場合は次のように、CMakeLists.txtUNICODE_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)

メモ帳を作る(notepad)

それでは、もう少し冒険してメモ帳を作ってみよう。 今度はダイアログアプリではない、普通のウィンドウアプリなので少しややこしくなる。 c:\dev\cxxnotepadというフォルダを作り、そこにCMakeLists.txtnotepad.cppnotepad_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. リソーエディタを開く。
  2. リソースの名前が1のアイコンを追加する。
  3. リソースの名前が1のマニフェストを追加する。
  4. 名前を「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

notepadビルド\

ビルドが完了したら、試しに起動してみる。

notepad起動\

何の変哲もないウィンドウが表示された。このウィンドウは、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_VISIBLEEDITコントロールのスタイルである。 ES_MULTILINEスタイルは複数行を意味する。WS_CHILDスタイルは、子ウィンドウであることを意味する。WS_VISIBLEスタイルはすぐに表示することを意味する。 WS_HSCROLLWS_VSCROLLスタイルはスクロールバーの表示を表している。

CreateWindowExが失敗すれば、OnCreateFALSEを返す。このとき親のメインウィンドウの作成は失敗する。

ninjaを再び実行して動作を確認しよう。

notepadを再び実行\

今度は文字が入力できる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にアイコンを付ける\

メインメニューを追加する

メインメニューを追加しよう。

  1. リソーエディタでnotepad_res.rcを開く。
  2. 「編集」メニューの「追加」→「メニューを追加」を選ぶ。
  3. 「リソースの名前」に「1」(いち)を指定して「OK」ボタンを押す。RT_MENU1日本語が追加される。
  4. 次のように書き換えて上書き保存する。
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を実行して確かめてみよう。

notepadにメニューを付ける\

メニューを追加したが、そのコマンドは定義されていないので、選択されても何も起こらない。メニューのコマンドを実装するには、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.txttarget_link_librariescomdlg32を追記する。

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+OCtrl+Sなどのアクセスキーを実装する。
  • ファイルドロップでファイルを開けるようにする。
  • ファイルを開くとき、保存するときに失敗したらエラーメッセージをちゃんと表示する。
  • ウィンドウアクティブ時にedt1にフォーカスを当てる。
  • edt1に等幅フォントを指定する。

アクセスキー

アクセスキーは、リソースから追加できる。

  1. リソーエディタでnotepad_res.rcを開く。
  2. 「編集」メニューから「追加」→「アクセスキーを追加」を選ぶ。
  3. 「リソースの名前」に「1」(いち)を指定して、「OK」ボタンを押す。
  4. 次のような内容になるよう編集し、上書き保存する。
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+OCtrl+Sが有効になった。

ファイルドロップの実装

次は、ファイルドロップを実装する。まず、WM_CREATE処理時に、ドロップを受け付けるように DragAcceptFiles(hwnd, TRUE);を呼ぶ。

次にWM_DROPFILESメッセージの処理を追加する。ウィンドウプロシージャWindowProcWM_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に入力できる。

等幅フォントを指定する

OnCreateedt1に等幅フォントを設定しよう。まずはフォントハンドルを保持する変数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
    }
}

上書き保存する。そして103108までのコマンドを実装しよう。 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+XCtrl+CCtrl+VDelCtrl+Aについては、 EDITコントロールにすでに実装されているので追加しなくてもよい。

ninjaを実行してちゃんと動作するか確認しよう。

人間に分かりやすいコマンドIDにする

100108のコマンドが実装されたが、番号だけでは何のことかわからない。 人間に分かりやすい識別子をコマンドIDにしてみよう。

  1. リソーエディタでnotepad_res.rcを開く。
  2. 次に、「表示」メニューから「リソースIDの一覧」を選ぶ。「リソースIDの一覧」ウィンドウが開かれる。
  3. 「リソースIDの一覧」の中を右クリックして、「追加...」を選ぶ。「リソースIDの追加」ダイアログが開かれる。
  4. 「IDの名前」に「ID_OPEN」と入力し、「整数」に「100」と入力して「OK」ボタンを押す。これで「ID_OPEN」という名前でコマンドID 100が追加された。
  5. 同様にして ID_SAVE101ID_EXIT102ID_UNDO103ID_CUT104ID_COPY105ID_PASTE106ID_DELETE107ID_SELECT_ALL108 を追加する。
  6. 上書き保存する。resource.hというファイルが作成される。
  7. notepad.cppの上の方に#include "resource.h"を追記する。
  8. 次のように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がだいぶ分かりやすくなった。

メモ帳に日時挿入の機能を追加する

メモ帳に日時を表す文字列を挿入する機能を追加してみよう。

  1. リソーエディタでnotepad_res.rcを開き、「リソースIDの一覧」ウィンドウを開く。
  2. 「リソースIDの一覧」の中を右クリックして、「追加...」を選ぶ。「リソースIDの追加」ダイアログが開かれる。
  3. 「IDの名前」に「ID_INSERT_DATETIME」と入力した後、「AUTO」ボタンを押してから「OK」ボタンを押す。これで「ID_INSERT_DATETIME」というコマンドIDが追加される。
  4. 「編集」メニューに「日時の挿入(&I)」というメニュー項目をID_INSERT_DATETIMEというコマンドIDで追加する。
  5. 上書き保存する。
  6. 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)

次はお絵かきソフト「ペイント」を作ろう。マウスでお絵かきできるかな。

初期のソース

「ペイント」の初期のソースをここに掲載する。

最初にヘッダファイル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は、背景を塗りつぶすブラシであり、ここでは灰色を選んでいる。

続いて OnCreateOnDestroyOnSizeOnCommandOnDropFilesWindowProc については、前回とほとんど同じで、解説は要らないだろう。

ビットマップとは

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構造体は、長方形領域を表し、 lefttoprightbottom という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;
}

OnCreateOnDestroyは、ちゃんと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の処理で描画を行うときは、描画コードをBeginPaintEndPaintで挟み込む。 BeginPaintは、HDC(デバイス コンテキストのハンドル)を返す。 このデバイスコンテキストというものは、画面や装置の一部の描画対象を表している。 これをうまく使えば、ウィンドウに描画することができる。

CreateCompatibleDC(NULL)はもう一つ別のデバイス コンテキスト(DC)を返す。これはメモリー上のDCである。 CreateCompatibleDC(NULL)を呼んだら、後でその戻り値をDeleteDCで破棄しないといけない。 hMemDCでビットマップのハンドル(hbm)をSelectBitmap関数で選択して、BitBltでビット群をhDCに転送すれば、 ビットイメージがhDCに転送される。これが画面への描画を引き起こす。 ビットマップを選択した後は、もう一度SelectBitmapを呼んで選択を元に戻した方がよい。

OnCreateOnDestroyOnPaintは、ちゃんと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_LBUTTONDOWNWM_MOUSEMOVEWM_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_bDraggingTRUEにして、SetCapture関数を呼ぶことでマウスドラッグを開始する。 そして位置をs_ptOldに保存している。SetCaptureを呼ぶと、マウスの動きの追跡を開始して、マウスポインタが ウィンドウから離れてもメッセージを吸い取るようになる(キャプチャー)。

次にOnMouseMoveでは、s_bDraggingがTRUEならば、 白いペンを使って、一つ前の位置から現在の位置まで、ビットマップ上に白い線を引いている。 MoveToEx関数は、現在の描画位置を移動して、LineTo関数は実際に線を引く関数である。 InvalidateRectは、ウィンドウの再描画を引き起こす。 そしてs_ptOldを更新している。

それからOnLButtonUpでは、ReleaseCaptureでキャプチャーを解放してドラッグを終了する。

WM_CAPTURECHANGEDは、キャプチャがEscキーなどでキャンセルされたときに呼ばれる。 WM_CAPTURECHANGEDメッセージはHANDLE_MSGで対応されていないのでこのようになる。

これで線が引けるはずである。ninjaして確認しよう。

ペイントで白い線\

ここまでのソースを次のリンクに掲載する。

ビットマップの読み書き

さて、ファイルの読み書きはpaint.cppDoLoadDoSave関数で行っていた。 次のようにする。

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)が下側に来るようにしている。

また、OnPaintOnMouseMoveOnLButtonUpの動作をモードに合わせないといけない。

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.cppDoNormalizeRect関数の定義の下に次のように書く。

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_hbmFloatings_rcFloatingを追加する。

static HBITMAP s_hbmFloating = NULL;
static RECT s_rcFloating;

選択して移動させた領域は、浮き上げる。 そこで、次のようなDoTakeOffDoLanding関数を追加する。 飛行機の「離陸」と「着陸」のようなイメージだ。

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.cppWM_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, "ファイルを開くのに失敗しました。"
}

...(以下略)...

お絵かきソフト(paint)のさらなる改良

前回でお絵かきソフトの土台ができたが、まだ機能が少なく貧弱である。 さらに機能を追加しよう。

モードの追加

では、「四角」「丸」「直線」の3つのモードを追加してみよう。

paint.hのMODE列挙型にMODE_RECTMODE_ELLIPSEMODE_LINEの3つを追加する。

enum MODE
{
    MODE_SELECT,
    MODE_PENCIL,
    MODE_RECT,
    MODE_ELLIPSE,
    MODE_LINE
};
extern MODE g_nMode;

リソーエディタで、リソースに ID_RECT110ID_ELLIPSE111ID_LINE112 の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_COLOR113ID_FILL_COLOR114 を追加する。

「ツール」メニューに「線の色(&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_SIZE115と、 リソースIDのIDD_SIZE100を追加し、 「ツール」メニューにメニュー項目「キャンバス サイズ(&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.cppOnCommandを、新しいコマンド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.cppOnCommandに新しいコマンド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.cppDoUpdateCanvasという関数を追加する。

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_HSCROLLWM_VSCROLLWM_MOUSEWHEELWM_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のキャンバスウィンドウクラスのRegisterClasshbrBackgroundにNULLを指定する。 これでWM_ERASEBKGNDをスキップすることができる。

次に、canvas.cppOnPaintを次のように書き換える。

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が非常に役立った。この場をもって感謝したい。