C++ Windows プログラミング⑥ 書いて覚えるための初心者自己中記事
アーキテクチャってなんだ?
構造とか構成のことか。
Windowsにもともと実装されていたグラフィックスインターフェイス
16ビットのWindows用に開発され32ビット・64ビットに移植された。
・GDI+
GDIの後継(詳細はちょっと意味わからない)
3Dグラフィックスをサポート
・Direct2D
GDI・GDI+の後継
・DirectWrite
テキストのレイアウトとラスター化のエンジン
ラスター化されたテキストの描画にはGDI・Direct2Dが使用できる。
・DirectXグラフィックインフラストラクチャ(DXGI)
出力用フレームなどの基礎レベルのタスクを実行する。
Direct2DとDirectWriteの仕様がおすすめらしい。
ハードウェアアクセラレーション
CPUではなくGPUによって実行されたグラフィックス計算を指す。
GDIはあまりこれをサポートしない。
Direct2Dはほとんどサポートするためパフォーマンスが優れる。
透過とアンチエイリアシング
Direct2Dはハードウェアアクセラレーションによるアルファブレンド(透過)の実行を完全にサポート。
GDIは微妙。
ハードウェアアクセラレーションによるアルファブレンドではアンチエイリアシングも有効化される。
エイリアシングは連続関数のサンプリングによって生じる不具合。
輪郭と背景をブレンドする方法でアンチエイリアシングは実行される。
Direct2D のアンチエイリアシング技術
次の画像は、それぞれの円の拡大図です。
上の画像の拡大図
凄いなぁ。
アンチエイリアシングはGDIだとほぼされない、GDI+だとされるけどCPUで実行、Dirext2DだとGPUでサポート。
ベクターグラフィックス
Direct2Dはベクターグラフィックスをサポート。
ベクターグラフィックスは数式を使用して線や曲線を表現するので画面の解像度に依存しない。pdfの文字とかがあれかな。
つぎ、デスクトップウィンドウマネージャー
Windows Vista以前のWindowsは画面の描写が直接行われていた。
(ビデオカードによって表示されるメモリバッファーにプログラムが直接書き込みを行っていた)
再描画による不具合を示すスクリーンショット
そのためこんな不具合があった。あ、なったことある。
別のウィンドウをドラッグしてウィンドウが重なったときとかに発生する。
これは両方のウィンドウの描画がメモリの同じ領域に対して実行されているから。
なんかいろいろあったんだね。
Vista以降はデスクトップウィンドウマネージャー(DWM)が導入されウィンドウの描画方法が根本的に変わった。
DWM によるデスクトップの合成方法を示す図
以前は上の図。ウィンドウはディスプレイバッファーに直接描画されていた。
今はDWMを使用して、オフスクリーンメモリバッファー(オフスクリーンサーフェイス)に描画され、DWMが画面に合成する。
つぎ、保持モードと直接モード
Direct2Dは直接モードAPI
保持モードAPIの例はWindows Presentation Foundatioin(WPF)がある。
保持モードはグラフィックスライブラリがシーンを保存して描画コマンドに変換する。アプリケーションはグラフィックライブラリに対して、シーンを変更するコマンドは出す。
直接モードはアプリケーションがシーンの描画コマンドを出す。
保持モード グラフィックスの図
直接モード グラフィックスの図
シーンとか描画コマンドとか知らんし。ここはこんな感じなのか~くらいで進んだほうがいいのかな。
つぎ。
初めてのDirect2Dプログラム
2D1D名前空間
Direct2Dのヘルパー関数とヘルパークラスが入っている。
色の値を構成する ClolrFクラス
変換行列を構成する Matrix3x2F
Direct2D構造体を初期化するための一連の関数
が入っている。
レンダーターゲット、デバイス、リソース
レンダーターゲットはプログラムの描画先のこと。
レンダーターゲットは通常、ウィンドウ。
画面に表示されないメモリ内のビットマップの時もある。
レンダーターゲットは ID2D1RenderTarget インターフェイスによって表される。
ソフトウェアデバイスはCPUを使用する。
アプリケーションはデバイスを作成しない。代わりにアプリケーションがレンダーターゲットを作成するときに暗黙的にデバイスが作成される。
リソースはプログラムが描画するために使用するオブジェクトのとこ。
Direct2Dで定義されているのは
ブラシ 線と領域の描画を制御(単色ブラシとグラデーションブラシがある)
ストロークスタイル 破線や実線など、線の外観を制御
ジオメトリ 直線と曲線の集合を表す
(ジオメトリはレンダリング前に変換する必要がある)
メッシュ 複数の三角形からなる図形
(メッシュデータはGPUで直接使用できる)
ブラシとメッシュはデバイス依存リソース。
デバイス依存(ハードウェア(GPU)ソフトウェア(CPU)いずれかの特定デバイスと対応付けられる)
ストロークスタイルとジオメトリはCPUメモリ内で維持される。
これらは
I2D1DResourceから派生したインターフェイスで表される。
例 ブラシは ID2D1Brushインターフェイス
Direct2Dファクトリオブジェクト
Direct2Dを使うには最初にDirect2Dファクトリオブジェクトのインスタンスを作成する。
ファクトリとはオブジェクトを作るためのオブジェクトの事。
Direct2Dファクトリオブジェクトが作るオブジェクトは
レンダーターゲット
残りのデバイス依存リソースはレンダーターゲットが作る。
このDirect2Dファクトリオブジェクトを作成する関数。
D2D1CreateFactory
ID2D1Factory *pFactory;
MainWindow() : pFactory(NULL)
case WM_CREATE: if (FAILED(D2D1CreateFactory( D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory))) { return -1; // CreateWindowEx が失敗する } return 0;
ID2D1Factoryインターフェイスへのポインタを用意して
NULLで初期化して
D2D1CreateFactory関数でファクトリオブジェクトを作成
オブジェクトへのポインタはいつも通り引数に入れておいて書き換えてもらう方法。
第一引数は、作成オプションフラグ。
Direct2Dを呼び出すのが単一スレッドなのか複数スレッドなのかでSINGLE / MULTIを変更する。
これらを行うのはWM_PAINTメッセージより前に行う必要があるので
WM_CREATEメッセージで行わせるといい。
ファクトリオブジェクトが作られたら次はリソースを作成。
今回は
レンダーターゲットとブラシ
class MainWindow : public BaseWindow<MainWindow> { ID2D1Factory *pFactory;//Direct2Dファクトリオブジェクトのインターフェイスへのポインタ ID2D1HwndRenderTarget *pRenderTarget;//Direct2Dリソース レンダーターゲットオブジェクトのインターフェイスへのポインタ ID2D1SolidColorBrush *pBrush;//Direct2Dリソース 単色ブラシオブジェクトのインターフェイスへのポインタ
ファクトリと同じようにインターフェイスへのポインタを用意。
MainWindow() : pFactory(NULL), pRenderTarget(NULL), pBrush(NULL)
コンストラクタで初期化
HRESULT MainWindow::CreateGraphicsResources()//D2D1ファクトリオブジェクトでリソースオブジェクトを作る { HRESULT hr = S_OK;//レンダーターゲットがすでにある場合にはS_OKを返す if (pRenderTarget == NULL) { RECT rc; GetClientRect(m_hwnd, &rc); D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom); hr = pFactory->CreateHwndRenderTarget(//ファクトリでレンダーターゲットオブジェクトを作成する D2D1::RenderTargetProperties(),//全種類のレンダーターゲットに共通するオプションを指定。D2D1RenderTargetProperties()は既定オプション D2D1::HwndRenderTargetProperties(m_hwnd, size),//ウィンドウハンドルとレンダーターゲットのサイズ &pRenderTarget);//書き換えてもらうインターフェイスへのポインタ if (SUCCEEDED(hr)) { const D2D1_COLOR_F color = D2D1::ColorF(1.0f, 1.0f, 0);//作成するブラシの色を格納した変数を指定 hr = pRenderTarget->CreateSolidColorBrush(color, &pBrush);//ブラシオブジェクト作成 if (SUCCEEDED(hr)) { CalculateLayout(); } } } return hr; }
GetClientRectとかはまだ説明ない。
リソースオブジェクトを作成したら描画する。
今回やることは
背景を単色で塗りつぶす
塗りつぶした円を描画する
レンダーターゲットがウィンドウなのでWM_PAINTメッセージが来たら処理する。
case WM_PAINT: OnPaint(); return 0;
void MainWindow::OnPaint() { HRESULT hr = CreateGraphicsResources();//ファクトリオブジェクトでリソース作りするメンバ関数。 if (SUCCEEDED(hr)) { PAINTSTRUCT ps; BeginPaint(m_hwnd, &ps); pRenderTarget->BeginDraw();//メソッドが描画の開始を通知 pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));//レンダーターゲット全体を単色で塗りつぶす pRenderTarget->FillEllipse(ellipse, pBrush);//指定されたブラシを使って塗りつぶした楕円を描画する ellipseにはCalculateLayoutメンバ関数でデータが格納されている hr = pRenderTarget->EndDraw();//描画の終了を通知 BeginDrawからここに来るまでのメソッドは戻り値がvoid* EndDrawの戻り値でまとめてHRESULTが返される if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET) { DiscardGraphicsResources(); } EndPaint(m_hwnd, &ps); } }
デバイス消失に対処する
ディスプレイの解像度を変更したり、ディスプレイアダプターを外したり。
そうなると、対応付けられていたリソース・レンダーターゲットも無効になる。
Direct2DではEndDrawの戻り値でD2DERR_RECREATE_TARGETという戻り値がありこれがデバイスの消失を通知する。
この戻り値を受け取ってしまったら、レンダーターゲットとデバイス依存リソースを再作成しなければならない。
なのでポインターを破棄する。
基本、レンダーターゲットやデバイス依存リソースはその都度破棄しない。
Direct2Dの描画ループ
- デバイス非依存のリソースを作成する。
- シーンをレンダリングする。
- レンダー ターゲットが存在するかどうかを確認する。存在しない場合は、レンダー ターゲットおよびデバイス依存リソースを作成する。
- ID2D1RenderTarget::BeginDraw を呼び出す。
- 描画コマンドを実行する。
- ID2D1RenderTarget::EndDraw を呼び出す。
- EndDraw が D2DERR_RECREATE_TARGET を返した場合は、レンダー ターゲットおよびデバイス依存リソースを破棄する。
- シーンの更新や再描画が必要な場合は、そのたびに手順 2 を繰り返す。
レンダーターゲットがウィンドウの場合はWM_PAINTメッセージが来たら都度2が実行される。
DPI(Dot Per Inch : 1インチ当たりのドット数)
DIP(Device Independet Pixel : デバイス非依存ピクセル)
DPIの説明のための予備知識
文字体裁
活字のサイズをポイントという単位で表す。
1ポイントは 1/72インチ
12ポイントは 12/72インチ == 1/6インチ
文字によって高さが違うのではみ出す奴が出てくる、それをカバーするために余白が必要。行間という。
上にはみ出す奴は行間で対応。
下にはみ出すやつは、そもそもの文字の位置をベースラインという少し上で表示させておき下にはみ出す奴だけベースラインを下に超える。
このベースラインの上下のエリアを
上ならアセント
下ならディセント
という。
ピクセルのサイズにはディスプレイの物理的な大きさと解像度が影響される。
しかしこれらはバラバラ。
なので72/72インチ == 1インチ == 72ポイントのフォントは1論理インチの高さ、と定義されている。
論理インチはピクセル数に変換され、Windowsでは96ピクセルが1論理インチとされている。
ユーザーがDPIを100% 150%と設定できる。
DWMによるスケール
プログラムがDPIをサポートしない場合は
UI要素の一部が表示されない
レイアウトが崩れる
ビットマップとアイコンがおかしくなる
マウスの座標がおかしくなる
DWMはプログラムがDPI非対応だとUI全体をスケールしてDPI設定(100%~)に一致させる。
ただし、ウィンドウが描画された後に適用されるのでぼやける。
DPI対応のアプリケーション
プログラム自体でDPI対応を宣言する。
これでDWMはスケールを実行しなくなる。
プログラムでDPI対応を宣言するにはアプリケーションマニュフェストを使用する。これはDLLやアプリケーションについて説明したXMLファイル。
マニュフェストにはDLLの依存関係
要求された特権レベル
そのプログラムが想定しているWindowsのバージョン
などが入っている。
プログラムが DPI 対応であることを宣言するには、次の情報をマニフェストに記述します。
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" > <asmv3:application> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware>true</dpiAware> </asmv3:windowsSettings> </asmv3:application> </assembly>
マニュフェストのほかの部分はVisual Studioのリンカーが作ってくれる。
上記の内容をマニュフェストに追加するVisual Studioでの方法。
- [プロジェクト] メニューの [プロパティ] をクリックします。
- 左側のペインで [構成プロパティ]、[マニフェスト ツール] を順に展開し、[入力と出力] をクリックします。
- [追加のマニフェスト ファイル] テキスト ボックスにマニフェスト ファイル名を入力し、[OK] をクリックします。
ファイルは .manifest
Direct2DはDPI設定と一致するように自動でスケールを実行する。
マウスとウィンドウの座標は物理ピクセルで提供される。
なのでピクセル座標をDPIに変換する必要がある。
DPI設定の値を取得するには
ID2D1Factory::GetDesktopDpiメソッドを呼ぶ。
float g_DPIScaleX = 1.0f; float g_DPIScaleY = 1.0f; void InitializeDPIScale(ID2D1Factory *pFactory) { FLOAT dpiX, dpiY; pFactory->GetDesktopDpi(&dpiX, &dpiY); g_DPIScaleX = dpiX/96.0f; g_DPIScaleY = dpiY/96.0f; } template <typename T> float PixelsToDipsX(T x) { return static_cast<float>(x) / g_DPIScaleX; } template <typename T> float PixelsToDipsY(T y) { return static_cast<float>(y) / g_DPIScaleY; }
レンダーターゲットのサイズを変更する。
ウィンドウのサイズが変更された場合はレンダーターゲットのサイズを変更する必要がある。
その後、レイアウトの変更、ウィンドウの再描画。
void MainWindow::Resize() { if (pRenderTarget != NULL) { RECT rc;//クライアントの情報を入れる構造体? GetClientRect(m_hwnd, &rc);//クライアントの物理ピクセル数を取得 D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom); pRenderTarget->Resize(size);//レンダーターゲットのピクセル数をResize CalculateLayout();//レイアウト計算メンバ関数 InvalidateRect(m_hwnd, NULL, FALSE);//再描画の実行 } }
void MainWindow::CalculateLayout() { if (pRenderTarget != NULL) { D2D1_SIZE_F size = pRenderTarget->GetSize();//レンダーターゲットのGetSizeメソッドはレンダーターゲットのサイズをDPI単位で返す。(GetPixelSizeだとピクセル数で返す) const float x = size.width / 2; const float y = size.height / 2; const float radius = min(x, y); ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), radius, radius); } }
つぎ、Direct2Dで色指定。
Direct2DはRGBカラーモデルを使用している。
赤、緑、青、アルファ(透明度)
これらを0.0~1.0までの浮動小数点値であらわす。
アルファは0.0で完全透明 1.0で完全不透明
赤 | 緑 | 青 | 色 |
---|---|---|---|
0 | 0 | 0 | 黒 |
1 | 0 | 0 | 赤 |
0 | 1 | 0 | 緑 |
0 | 0 | 1 | 青 |
0 | 1 | 1 | シアン |
1 | 0 | 1 | マゼンタ |
1 | 1 | 0 | 黄 |
1 | 1 | 1 | 白 |
色を表現する構造体D2D1_COLOR_F
D2D1_COLOR_F 構造体から派生した、D2D1::ColorF クラスを使って色を指定することも可能です。
// 前の例に同じ D2D1::ColorF clr(1, 0, 1, 1);
アルファブレンドは前景色と背景色をうまいこと混ぜる。
色 = αf Cf + (1–αf) Cb
Cf == 前景色
Cb == 背景色
af == 前景色のアルファ値
さっきからずっとぼんやり。
座標変換を適用する。
pRenderTarget->BeginDraw();//メソッドが描画の開始を通知 pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));//レンダーターゲット全体を単色で塗りつぶす pRenderTarget->FillEllipse(ellipse, pBrush);//指定されたブラシを使って塗りつぶした楕円を描画する ellipseにはCalculateLayoutメンバ関数でデータが格納されている hr = pRenderTarget->EndDraw();//描画の終了を通知 BeginDrawからここに来るまでのメソッドは戻り値がvoid* EndDrawの戻り値でまとめてHRESULTが返される
レンダーターゲットのFillEllipseメソッドでX軸Y軸に沿って楕円を描画した。
ほかの形、
傾斜した楕円を示す図
こんなばあいの方法。
座標変換を行うと
- 点を中心とした回転
- スケール(拡大縮小)
- 平行移動 (X 軸または Y 軸方向への変位)
- 傾斜 (または "傾き" とも呼ばれる)
こんな風に出来る。
座標変換とは一連の点を新たな一連の点とマッピングする数学演算。????
点を中心とした回転を示す図
それぞれの点が新たな点にマッピングされた。
座標変換は行列を使用して実装される。
Direct2Dで座標変換を適用するには
ID2D1RenderTarget::SetTransFormメソッドを呼び出す。
このメソッドでD2D1_MATRIX_3X2_F構造体を使用して定義する。
D2D1::Matrix3x2Fクラスで??各メソッドを呼び出すとこの構造体を初期化できる。??
このクラスは座標変換に対応する行列を返す以下のメソッドをもつ、
pRenderTarget->SetTransform(
D2D1::Matrix3x2F::Rotation(20, D2D1::Point2F(100,100)));
100、100の点を中心に20度の回転
この座標変換は後続の描画にも適用される。
pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
解除はこれ。
~~~~~~~~~~~~~~
あぁーだめだ。わけわからなくなった。
勉強しなおそう。
ここまで。