2024/05/04

フォーム上に図形や文字をGDI描画




ユーザーフォームはユーザーとの情報のやり取りには欠かせない道具です。フォーム上には様々なコントロールを自由に配置でき、Imageコントロール等ではシート上のグラフや図形などを画像として表示することも可能です。
しかしコントロールを通してでは無く、Win32API関数を使用すれば「直接ユーザーフォーム上に図形や文字を描画」することが可能です。
Windowsのグラフィック関係のAPIをGDI(Graphics Device Interface)と言うようですが、今回はユーザーフォーム上に図形や文字を描画するGDI関数について紹介をします。

1.GDI描画の流れ

GDIで描画をするための手順を図01で整理します。ここでは「Excelのユーザーフォーム上に描画」する関数の内で代表的なもの(図01の赤文字の関数)を載せていますが、この他の関数も多数存在します。
描画作成の流れ
図01

上図に沿って、流れを説明していきます。
まず、FindWindow関数で目的のウィンドウ(今回の場合はExcelのユーザーフォーム)のハンドルを取得します。「ハンドル」とは、プログラム内で使う固有のID番号みたいなものです。
その後、そのウィンドウの描画属性情報(=デバイスコンテキスト:Device Contexts)のハンドルをGetDC関数で取得します。これ以降の描画準備や描画実施時には、このデバイスコンテキスト(DC)のハンドルが使われます。
描画する対象が決まった後は、描画道具の選択(SelectObject関数)をします。選択する道具には、ペン(直線や図形の外枠の描画)・ブラシ(図形内の塗りつぶし)・フォント(文字のFontやサイズ、斜体など)などがありますので、SelectObjectの前にCreatePenなどの関数を使って道具を作っておきます。
描画道具が選択できたら描画を開始します。線・図形・文字では描画の方法が異なりますので、各描画に合った関数を使用することになります。
なお前に描画したものが不要であれば、描画作業の前に「Repaintメソッド」で削除しておきます。削除しなければ重ね書きになります。(なお、ユーザーフォームの起動直後に描画を行う場合は、一旦Repaintの実行が必要。詳細「よりみち」参照)
描画が完了したら、SelectObject関数で描画道具を元(既定の道具)に戻します。
なお描画最後に「描画道具を元に戻す」という行動は、必須の作業ではありません。例えば異なる線種を次々に描画する場合、次々に違う線種の道具に交換するのですから、いちいち元に戻すのは手間が1つ多いことになります。
しかし描画道具には「既定の道具」が存在します。つまりSelectObject関数を使わなくても描画は可能なのです(「よりみち」参照)。ですので描画の後で道具を元に戻さないクセがあると、もし次の工程に「既定の道具で描画」の工程があった場合に、既定ではなく前回の道具での描画となってしまう事になります。
描画が全て完了したら、DeleteObjectで作成した描画道具を削除し、ReleaseDCでDC(=デバイスコンテキスト)をメモリから解放します。

2.各ハンドルの取得と解放

2-1.ウィンドウハンドルの取得

まず描画対象のハンドルを取得するのが図02のFindWindow関数です。
第1引数には「対象のクラス名」を、第2引数にはその「対象のウィンドウ名」を指定します。
役割関数名宣言
引数引数の内容構造体戻り値
対象ハンドルを取得FindWindowDeclare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
lpClassName
lpWindowName
ウィンドウのクラス名
ウィンドウ名
成功=ウィンドウハンドル(以下ではhWnd)
失敗=0
図02

今回の場合、第1引数である描画対象は「Excelのユーザーフォーム」ですので、対象のクラスは「ThunderDFrame(Excel97より前の場合は ThunderXFrame)となります。なおExcel本体のクラス名は「XLMAIN」です。
第2引数のウィンドウ名は、ユーザーフォームの場合は下図の左側ような「フォーム名」ですので「Me.Caption」とする事でウィンドウ名が取得できます。一方Excel本体のウィンドウ名は下図の右側のように「ブック名 - Excel」となります。
(なお「ハイフンの両脇」は、それぞれ半角スペースを入れる必要があります。なお大文字小文字は区別しないようです。)
Window名
図03

もし第1引数にvbNullString(値ゼロの文字列=Null相当の文字列)を指定した場合は、第2引数に指定したウィンドウ名を持つウィンドウが対象となります。反対に第2引数をvbNullStringにすると、第1引数で指定したウィンドウで最も前面にあるものが対象となるようです。
なお第1と第2の両方をvbNullStringにすると、最前面にあるウィンドウが対象となる はずなのですが、試したところ「何のウィンドウのハンドルを取得したのか判らない(=ハンドルからウィンドウを特定出来なかった)」結果となり、描画は行われませんでした。第1・第2とも、分かるのであれば出来るだけ指定した方が良さそうです。

2-2.DC(デバイスコンテキスト)の取得と解放

ウィンドウのハンドルが取得できたら、次はデバイスコンテキスト(DC)のハンドルを取得します。デバイスコンテキストとは、FindWindow関数で得た描画対象ウィンドウの「描画属性情報を保持している構造体」というのが正確な説明のようです。描画のためのペンやブラシを指定する対象であることから、描画道具を持つための「手」と表現しているサイトもあります。
そのデバイスコンテキストを取得・解放する関数が以下になります。
役割関数名宣言
引数引数の内容構造体戻り値
デバイスコンテキスト(DC)を取得
(クライアント領域のみ)
GetDCDeclare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
hWnd取得対象ウィンドウのハンドル成功=DCハンドル(以下ではhDC)
失敗=0
DCを取得
(非クライアント領域を含む)
GetWindowDCDeclare PtrSafe Function GetWindowDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
hWnd取得対象ウィンドウのハンドル成功=DCハンドル(以下ではhDC)
失敗=0
DCの解放ReleaseDCDeclare PtrSafe Function ReleaseDC Lib "user32" _
(ByVal hwnd As LongPtr, ByVal hdc As LongPtr) As Long
hWnd
hDC
対象ウィンドウのハンドル
解放対象のDCハンドル
成功=1
失敗=0
図04

デバイスコンテキストを取得する関数には「GetDC」「GetWindowDC」の2種があります。両方ともデバイスコンテキストのハンドルを戻す事は同じですが、その範囲は図05のように異なります。
GetDCとGetWindowDCで得られる範囲の違い
図05

ウィンドウ(今回はユーザーフォーム)の範囲には「クライアント領域」と「非クライアント領域」があります。クライアント領域は、ユーザー側がボタンを配置したり描画を作成したりできる範囲(赤の点線枠)です。非クライアント領域はスクロールバーや境界線、タイトルバーやメニューバーを指し、ユーザー側から直接制御が不可能な範囲(青点線枠と赤点線枠の間)です。
GetDC関数ではクライアント領域(赤点線枠)のハンドルを戻しますが、GetWindowDC関数では非クライアント領域も含めた範囲(青点線枠)のハンドルを戻すことになります。
どちらの関数で取得しても良いですが、非クライアント領域には図05の一番右側のように「描画が出来ない(=描画実行しても描画されない)」ので、原点位置を理解し「クライアント領域に入るように描画作業」をする必要があります。
図05の一番右は、GetWindowDCでDCを取得したが、クライアント領域と思って作業してしまった時の結果です。)
また取得したデバイスコンテキストは、必要が無くなったら必ずReleaseDC関数でメモリーから解放する必要があります。解放しないとメモリー上に残ってしまい、パフォーマンスの低下や最悪動作異常になるようです。
一方FindWindow関数で得たウィンドウハンドル(変数hWnd)の方は、解放の手続きは不要のようです。単に見つける("Find")だけだからかもしれません。

2-3.描画用のペンやブラシ、フォントのハンドル

描画道具のハンドルをSelectObject関数に指定する事で「描画する道具を指定」することが出来ます。その前に描画道具を準備し、ハンドルを取得しておく事が必要です。描画道具にはペン・ブラシ・フォントがあります。

2-3-1.ペンの指定

描画用のペンの種類を指定し、そのハンドルを戻すのが図06の「CreatePen」または「CreatePenIndirect」です。前者はペンのスタイル・幅・色を関数の引数に直接指定し、後者は同じ内容をLOGPEN構造体として指定するものです。
役割関数名宣言
引数引数の内容構造体戻り値
ペンを作成CreatePenDeclare PtrSafe Function CreatePen Lib "gdi32" _
(ByVal nPenStyle As Long, ByVal nWidth As Long, ByVal crColor As Long) As LongPtr
nPenStyle
nWidth
crColor
ペンのスタイル
ペンの幅
ペンの色
成功=ハンドル
失敗=0
ペンを作成
(CreatePenの
構造体Ver)
CreatePenIndirectDeclare PtrSafe Function CreatePenIndirect Lib "gdi32" (lpLogPen As LOGPEN) As LongPtr
lpLogPen[ペンの情報 LOGPEN]
 ペンのスタイル
 幅(POINTAPI構造体)
 色
Type LOGPEN
 lopnStyle As Long
 lopnWidth As POINTAPI
 lopnColor As Long
End Type
Type POINTAPI
 x As Long
 y As Long
End Type
成功=ハンドル
失敗=0
図06

第1引数・第1要素のペンの「スタイル」には、以下のような値を指定します。なおVBAではPS_SOLID等の定数は定義されていないため、事前に定数設定をしておくか、または値を直接指定します。
(定数)内容
PS_SOLID0実線
PS_DASH1破線(ペン幅が1以下で有効)
PS_DOT 2点線(ペン幅が1以下で有効)
PS_DASHDOT3一点鎖線(ペン幅が1以下で有効)
PS_DASHDOTDOT4二点鎖線(ペン幅が1以下で有効)
PS_NULL5空のペン。描画は行われない
PS_INSIDEFRAME6実線(ペン幅が1超で有効)
フレーム境界線の時、フレーム内に収まる様に描画
図07

第2引数のペンの「幅(≒太さ)」は、ピクセル単位の整数で指定します。なおゼロを指定すると1を指定した事になります。
なおLOGPEN構造体のペン幅(第2要素)には、POINTAPI構造体のXの方に値を入れます。その場合Y側は空でOKです。
またスタイルとしてPS_DASH等(値として1~4)は「幅が1以下で有効」ですが、そのスタイルの場合にペン幅として2以上を指定してしまうと、スタイルにPS_SOLIDを指定した事になってしまいますので注意が必要です。
第3引数・第3要素のペンの「色」は、RGB関数などでLong型の値を指定します。
ペンのスタイル・幅・色のサンプルを下記に示します。
ペンのスタイル・幅・色の一覧
図08

寄り道(コスメティックペンは不可能?)
ペンには「ジオメトリック(幾何学的)ペン」と「コスメティック(装飾的)ペン」の2種があり、上記で説明したCreatePen関数とCreatePenIndirect関数は、ジオメトリックペンとなります。
一方、コスメティックペンはExtCreatePen関数で設定できるはずなのですが、どのように設定しても正しいハンドルを戻してくれませんでした。
コスメティックペンは「斜線や曲線を描画する場合に滑らかな線を描画できる」との説明だったので、結構期待していたのですが、ExcelのVBAでは不可能なのかもしれません。残念です。

2-3-2.ブラシの指定

描画用のブラシの種類を指定し、そのハンドルを戻すのが図09です。
役割関数名宣言
引数引数の内容構造体戻り値
ブラシ作成
(塗りつぶし)
CreateSolidBrushDeclare PtrSafe Function CreateSolidBrush Lib "gdi32" (ByVal crColor As Long) As LongPtr
crColorブラシの色成功=ハンドル
失敗=0
ブラシ作成
(ハッチ)
CreateHatchBrushDeclare PtrSafe Function CreateHatchBrush Lib "gdi32" _
(ByVal nIndex As Long, ByVal crColor As Long) As LongPtr
nIndex
crColor
ハッチスタイル
ブラシの色
成功=ハンドル
失敗=0
ブラシ作成
(塗りつぶし
or ハッチ)
CreateBrushIndirectDeclare PtrSafe Function CreateBrushIndirect Lib "gdi32" _
(lpLogBrush As LOGBRUSH) As LongPtr
lpLogBrush[ブラシの情報 LOGBRUSH]
 ブラシスタイル
 ブラシの色
 ハッチスタイル
Type LOGBRUSH
 lbStyle As Long
 lbColor As Long
 lbHatch As LongPtr
End Type
成功=ハンドル
失敗=0
図09

CreateSolidBrush関数は「指定した色で単純に塗りつぶす」機能です。引数のブラシの色には、RGB関数等でLong型の値を指定します。
CreateHatchBrush関数は、塗りつぶしでは無く「ハッチング(≒模様)」をする機能です。第1引数には、以下の表内から「ハッチスタイル」を指定します。また第2引数には、上記同様に色を指定します。
(定数)内容
HS_HORIZONTAL0水平ハッチ
HS_VERTICAL1垂直ハッチ
HS_FDIAGONAL245度右下がりのハッチ
HS_BDIAGONAL345度右上がりのハッチ
HS_CROSS4水平と垂直のクロスハッチ
HS_DIAGCROSS545度のクロスハッチ
図10

CreateBrushIndirect関数にはLOGBRUSH構造体で情報を渡します。構造体内にはスタイル・色・ハッチスタイルの3要素があり、第1要素で「塗りつぶし」または「ハッチング」を切り替えられ、ハッチングを選んだ場合は第3要素でハッチ種を選べます。
第1要素「lbStyle」には、ブラシのスタイルを図11から指定します。
(定数)内容
BS_SOLID0純色のブラシ
BS_HOLLOW または BS_NULL1透明ブラシ
BS_HATCHED2ハッチブラシ
BS_PATTERN3メモリビットマップのパターンブラシ(BMPのハンドル)
BS_DIBPATTERN5DIB※1仕様のパターンブラシ(DIBのハンドル)
BS_DIBPATTERNPT6DIB仕様のパターンブラシ(DIBへのポインタ)
※1 DIB(Device-Independent Bitmap)=デバイスに依存しないビットマップ
図11

BS_SOLID(値=0)は、第2要素(lbColor)で指定した色で塗りつぶします。
BS_HOLLOW or BS_NULL(値=1)は、塗りつぶしやハッチングをせず、元の背景(ここではフォームの背景)を残します。
例えば楕円形をEllipse関数で描画させると「楕円の内部は塗りつぶし(既定では白色)」されてしまいますが、この「透明ブラシ」を使用することで「外周の枠のみ」を描画することが出来ます。
BS_HATCHED(値=2)は、第2要素(lbColor)で指定した色で、第3要素(lbHatch)に指定する模様(図10)をハッチングします。
なお「BS_PATTERN」より下の3つのブラシの使い方については、調べてはみたのですが良く分かりませんでした。VBAでは使用出来ないのかもしれないので、文字色を淡くしてあります。
第2要素「lbColor」には、ブラシの色を指定します。
第3要素「lbHatch」は、第1要素にBS_HATCHED(値=2)を指定した時は、図10の中からハッチスタイルを選択します。
なお第1要素にBS_SOLID(値=0)を指定した時は、第3要素は無視されます(何を指定しても塗りつぶし)。
また第1要素にBS_HOLLOW または BS_NULL(値=1)を指定した時も、第3要素は無視です(何を指定しても非表示)。
3つの関数で指定する「ブラシの色」「ハッチスタイル」「ブラシスタイル」を振って描画したのが下記です。
ブラシ色・ハッチスタイル・ブラシスタイルの一覧
図12

一番右側はCreateBrushIndirect関数のもので、LOGBRUSH構造体のブラシの色には黒色、ハッチスタイルにはHS_DIAGCROSS(値=5)を設定した上で、ブラシスタイルを0・1・2 と振ったものです。
寄り道(CreateBrushIndirect関数の奇妙な現象)
CreateBrushIndirect関数のLOGBRUSH構造体の第1引数に「ハッチブラシ(値=2)」を指定し、第3引数に図10の範囲を超えた値を指定すると、以下のような奇妙な現象が発生します。第3引数は0~5の範囲と決められています。
 ①第3引数=6~7 ・・・第2引数に指定した色で塗りつぶし
 ②第3引数=8~9 ・・・赤色で塗りつぶし
 ③第3引数=10~ またはマイナス値・・・白色で塗りつぶし
①は「指定値以外を指定したので、ハッチでは無く塗りつぶし」となるのは分かるのですが、②③は全く理解できません。もしプログラムでハッチ種類を切り替える際は御注意下さい。
また第1要素にBS_DIBPATTERN(値=5)やBS_DIBPATTERNPT(値=6)を指定した場合は、第3要素には「DIB(デバイスに依存しないビットマップ)へのハンドル」や「DIBへのポインタ」を指定するようですが、ビットマップの取り扱い方法自体が良くわからないため、説明を省いています。

ブラシには、これ以外にも「CreatePatternBrush」や「CreateDIBPatternBrush」などの関数があるようです。両方とも図11の下半分と同様、ビットマップの絵を繰り返し貼り付けて模様にする機能のようですが、VBAとしてどう設定すれば良いのか辿り着けませんでした。

2-3-3.フォントの指定

文字を描画するためのフォントを指定し、そのハンドルを戻すのが図13です。「CreateFont」は引数に直接フォント情報を指定するのに対し、「CreateFontIndirect」はLOGFONT構造体に格納した後まとめて指定する方法です。内容は同一のようです。
なお、これ以外にもフォント指定の関数はいくつかありそうです。
役割関数名宣言
引数引数の内容構造体戻り値
論理フォント
を作成
CreateFontDeclare PtrSafe Function CreateFont Lib "gdi32" Alias "CreateFontA" _
(ByVal H As Long, ByVal W As Long, ByVal E As Long, ByVal O As Long, _
ByVal W As Long, ByVal i As Long, ByVal u As Long, ByVal S As Long, _
ByVal C As Long, ByVal OP As Long, ByVal CP As Long, ByVal Q As Long, _
ByVal PAF As Long, ByVal F As String) As LongPtr
H
W
E
O
W
I
u
S
C
OP
CP
Q
PAF
F
文字の高さ
平均文字幅(論理単位)
文字送り方向とX軸の角度(1/10度単位)
各文字のベースラインとX軸の角度(1/10度単位)
フォント太さ(0=既定、400=標準、700=太字)
斜体か否か(斜体=True)
下線有無(下線付き=True)
取消線有無(取消線有り=True)
フォント文字セット(既定=DEFAULT_CHARSET)
実出力精度(既定動作=OUT_DEFAULT_PRECIS)
クリップ精度(既定動作=CLIP_DEFAULT_PRECIS)
出力品質(論理Fontと実物理Fontの一致度)
Fontのピッチとファミリ(フォントの見た目)
Font名の文字列
成功=ハンドル
失敗=0
論理フォント
を作成
(CreateFont
の構造体Ver)
CreateFontIndirectDeclare PtrSafe Function CreateFontIndirect Lib "gdi32" Alias "CreateFontIndirectA" _
(lpLogFont As LOGFONT) As LongPtr
lpLogFontフォント情報(LOGFONT構造体)
 文字セルまたは文字の高さ
 平均文字幅
 文字送りの方向とX軸との角度
 ベースラインとX軸との角度
 フォントの太さ
 イタリック体指定
 下線付き指定
 打ち消し線付き指定
 キャラクタセット
 出力精度
 クリッピングの精度
 出力品質
 ピッチとファミリ
 フォント名
Type LOGFONT
 lfHeight As Long
 lfWidth As Long
 lfEscapement As Long
 lfOrientation As Long
 lfWeight As Long
 lfItalic As Byte
 lfUnderline As Byte
 lfStrikeOut As Byte
 lfCharSet As Byte
 lfOutPrecision As Byte
 lfClipPrecision As Byte
 lfQuality As Byte
 lfPitchAndFamily As Byte
 lfFaceName As String
End Type
成功=ハンドル
失敗=0
図13

文字高さ(H、lfHeight)は、いわゆるフォントサイズ(文字の縦サイズ)をピクセル単位で指定します。通常VBA内ではフォントはポイント単位で扱っていますので、注意が必要です。
平均文字幅(W、lfWidth)は、文字の横サイズですが、ゼロ値を指定することで条件に最も近い値が自動的に選択されます。
ちなみに14ポイント(ピクセルだと、14/0.75)の文字に対して、lfHeight・lfWidthを指定したのが図14の上段になります。
フォントの高さと幅の指定方法による描画違い
図14

「lfHeightのみを指定」したのが左側で通常の文字表示です。一方「lfWidthに高さの半分を指定」すると中央の状態になりますが、少しゴシックっぽくなっているのが分かるかと思います。
これは、まず高さとしての14ポイントをピクセルに変換すると、14 ÷ 0.75 = 18.67ピクセルとなりますが、lfHeightはLong型ですので小数点は無く「19ピクセルを指定」した事になります。また幅(lfWidth)は高さの約半分なので 14 ÷ 0.75 ÷ 2 = 9.33ピクセルとなり、「9ピクセル」に丸められます。
つまり「高さ:幅=19 : 9 」を指定した事になり、文字のバランスが崩れてゴシックっぽく見えるのではないかと考えられます。
一方図14の下段は、14ポイントに近い「高さ=18ピクセル」で描画したものですが、この2:1を守った設定だとゴシックっぽくなることなく、中央下段のように正しく表示されるようです。
また、文字幅に文字高さと同じ値を指定すると、一番右のように「2倍幅」の文字になります。但し一番右の上と下とでは少し文字幅が異なって見えますが、これが19ピクセルと18ピクセルの差なのか否かは良く分かりません。
文字送り方向とX軸との角度(E、lfEscapement)は、文字列全体をX軸に対し何度傾けるかの設定です。1/10度単位ですので、10度傾斜の時は値=100を指定します。
ベースラインとX軸との角度(O、lfOrientation)は、文字1つ1つをX軸に対し何度傾けるかの設定のようです。こちらも1/10度単位の設定です。
各設定によりどの様な描画になるのかを確かめたのが図15ですが、ユーザーフォーム上での設定では「lfEscapementが有効」で「lfOrientationは無視」されるようです。ただし、基本的には「lfEscapement」と「lfOrientation」とには同じ値を指定するように との事です。
lfEscapement値とlfOrientation値の組み合わせでの文字の角度の変化
図15

寄り道(グラフィックモード)
この「lfEscapement」と「lfOrientation」の設定値は「グラフィックモードがGM_ADVANCEDの時には、別々に設定可」と説明されています。これを確かめるため、GetGraphicsMode関数でユーザーフォームのモードを調べてみると、GM_COMPATIBLE(値=1)でした。
つまりExcelのユーザーフォームは「16ビットWindowsと互換性のあるモード」で古いタイプのために、細かな設定が出来ないという事のようです。
今回ユーザーフォーム以外は調べていませんがGM_ADVANCEDモードの場所が存在すれば、文字を階段状に表示するような事も可能かもしれません。

フォント太さ(W、lfWeight)は、フォントの太さを表す0~1000の値を指定します。既定太さにするには「0」を指定します。太さは以下のように分類されています。
(定数)
FW_DONTCARE0
FW_THIN100
FW_EXTRALIGHT, FW_ULTRALIGHT200
FW_LIGHT300
FW_NORMAL, FW_REGULAR400
(定数)
FW_MEDIUM500
FW_SEMIBOLD, FW_DEMIBOLD600
FW_BOLD700
FW_EXTRABOLD, FW_ULTRABOLD800
FW_HEAVY, FW_BLACK900
図16

VBAでは定数は直接扱えませんが、図16の定数名を見ると「文字を徐々に太く表示」出来るようなイメージを持ってしまいます。そこで、実際にlfWeight値をスライドしてみたのが図17です。文字高さ(lfHeight)もいくつか試してみました。
lfWeightでの文字太さの変化
図17

図17で分かるのは「400と700の間で切り替わり、太さの種類としては2種類」ということです。切り替わりをもう少し詳しく調べてみると「550以下が標準」「551以上が太字」となっています。
イタリック体(I、lfItalic)下線付き(U、lfUnderline)打ち消し線(S、lfStrikeOut)は、Excelのワークシートでも良く使われる機能です。各項目をON(True 又は 1 を指定)にすることで、図18のような表示となります。
イタリック体、下線付き、打ち消し線付きでの文字太さの変化
図18

キャラクタ文字セット(C、lfCharSet)は、描画する文字が何語なのかを指定します。下記表内から選択します。
(定数)内容
ANSI_CHARSET0ANSI文字
DEFAULT_CHARSET1指定なし
SYMBOL_CHARSET2標準シンボル
MAC_CHARSET77Macintosh文字
SHIFTJIS_CHARSET128シフトJIS(日本語)
HANGUL_CHARSET129ハングル文字
JOHAB_CHARSET130韓国語文字(Johab コード)
GB2312_CHARSET134中国語の簡体字
CHINESEBIG5_CHARSET136中国語の繁体字
(定数)内容
GREEK_CHARSET161ギリシャ語文字
TURKISH_CHARSET162トルコ語文字
HEBREW_CHARSET177中東語(ヘブライ語)文字
ARABIC_CHARSET178中東語(アラビア語)文字
BALTIC_CHARSET186バルト語文字
RUSSIAN_CHARSET204キリル文字
THAI_CHARSET222タイ語文字
EASTEUROPE_CHARSET238東欧諸国の発音区別符号
OEM_CHARSET255O/S依存の文字
図19

Microsoftでは「(Windows NT/2000以降は)DEFAULT_CHARSETを指定すると、文字セットは現在のシステムロケール(言語と地域)に基づいた値に設定される」とあり、PCが日本語設定になっている場合は「SHIFTJIS_CHARSET」となるようです。
但し安定的に使用するには「DEFAULT_CHARSET以外をキチンと選べ」とも書いてありますので、日本語を描画する場合は「SHIFTJIS_CHARSET(値=128)」、欧文の場合は「ANSI_CHARSET(値=0)」を指定するのが良さそうです。
出力精度(OP、lfOutPrecision)は、描画するフォントの精度を指定します。
「指定したフォントと、実際に描画するフォントをどの程度一致させるかを下記表から選択」と、Microsoftや他サイトでは説明されています。
(定数)内容
OUT_DEFAULT_PRECIS0デフォルトの動作に任せる
OUT_STRING_PRECIS1使用せず ※2
OUT_CHARACTER_PRECIS2使用せず
OUT_STROKE_PRECIS3使用せず ※2
OUT_TT_PRECIS4同名フォントが複数存在する場合、TrueTypeフォントを選択
OUT_DEVICE_PRECIS5同名フォントが複数存在する場合、デバイスフォントを選択
OUT_RASTER_PRECIS6同名フォントが複数存在する場合、ラスタフォントを選択
OUT_TT_ONLY_PRECIS7TrueTypeフォントのみを選択。TrueTypeが無い場合はデフォルト動作
OUT_OUTLINE_PRECIS8TrueTypeフォントやその他のアウトラインベースのフォントを選択
図20

正直、何を言っているのか全く分かりません。フォントの知識が乏しい事も原因かと思いますが、説明の中の「出力精度」「一致度合」に対して、表の内容がまさに一致していないのです。
そこでlfOutPrecision値を振って描画し、出力される文字を見比べてみました。まずはラスタフォントです。
ラスタフォントでOutPrecision値を変化させて描画
図21

「Courier」はラスタフォント(=ビットマップフォント)で、塗り潰す座標をデータとして持っているフォントですので、図21のように「目の粗い文字」となっています。
但し「OUT_TT_ONLY_PRECIS(値=7)」だけはスムーズな文字です。この値の説明は図20では「TrueTypeフォントのみを選択。TrueTypeが無い場合はデフォルト動作」となっています。つまりCourierフォントはTrueTypeでは無いためデフォルトフォント(TrueType?)に切り替わり、スムーズな文字になっていると思われます(この「デフォルト」が、どのフォントなのかは良く分かりません)。
ちなみにTrueTypeフォントを使い、同様にlfOutPrecision値を振って描画したのが下記です。
TrueTypeフォントでOutPrecision値を変化させて描画
図22

図22は「MS ゴシック」、つまりTrueTypeフォントなので、どのlfOutPrecision値でも同じ文字の描画となるようです。
なお図22の字体は、図21のOUT_TT_ONLY_PRECIS(値=7)の字体とは違う(図22の方が細長く、Iの字の上端・下端に張りが出ている)ため、デフォルトフォントはMS ゴシックでは無さそうです。
以上から、lfOutPrecisionは「出力精度」とか「実フォントと指定フォントの一致度」というよりも、「複数フォントが候補となった場合、その絞り込み方法」という説明の方が正しい気がします。
そこで実際問題としてlfOutPrecision引数にどの値を指定するかですが、図21の結果からみると「OUT_TT_ONLY_PRECIS(値=7)」となります。しかし例えば、フォントとして意図して「Courier(ラスタフォント)」を指定した時には、そのフォント指定は「全く無視」されてしまう結果となります。フォント種類としてTrueTypeやOpenTypeが多いことを考えると「OUT_DEFAULT_PRECIS(値=0)」で良いのではと思います。
なおWin32APIのEnumFontFamiliesEx関数で「システム内の名前付きフォントの一覧を取得」する事が出来ますが、その時には以下のような値が戻り、その値は図20の※2の値に対応するもののようです。
 ・ラスタフォント(=ビットマップフォント)の時 →「OUT_STRING_PRECIS(値=1)」
 ・TrueTypeフォント、アウトラインベースのフォント、ベクタフォントの時 →「OUT_STROKE_PRECIS(値=3)」
 (Windows 95/98の場合にTrueTypeフォントやベクタフォントの時 →「OUT_STROKE_PRECIS(値=3)」)
クリッピング精度(CP、lfClipPrecision)は、「クリッピング領域の一部外にある文字をクリップ」する方法を指定します。指定の方法は、図23の項目の中から1つ又は複数を選択します。
(定数)内容
CLIP_DEFAULT_PRECIS0既定の動作に任せる。
CLIP_CHARACTER_PRECIS1使用しない。
CLIP_STROKE_PRECIS2 使用しない ※3
CLIP_MASK15使用しない
CLIP_LH_ANGLES16全フォントの回転方向が座標系の方向に従う
CLIP_TT_ALWAYS32使用しない
CLIP_EMBEDDED
(CLIP_ENCAPSULATE ?)
128読み取り専用の埋め込みフォント使用時は必須
図23

「クリッピング領域」とは、今回対象としているExcelのユーザーフォームでいうと、図24の「描画が可能な範囲」であり、図05で示した「クライアント領域」と同じ範囲を示しているようです。
クリッピング領域の範囲
図24

また「クリッピング領域外の文字をクリップする」ということは、非クライアント領域(図05参照)には描画させないように「文字を切り取る」という意味のようです。(間違っていたら、ごめんなさい)
この考え方を確認するために、ユーザーフォーム上に描画した最後の文字が中途半端に隠れるようにして、上の表(図23)の各値を指定してみたのですが、描画された文字には全く変化ありませんでした。
なお「CLIP_LH_ANGLES(値=16)」と「CLIP_EMBEDDED(値=128)」の内容については、今回のExcelでは無関係そうなので深くは調べませんでしたが、Excelのユーザーフォーム上で文字を描画するだけであれば「CLIP_DEFAULT_PRECIS(値=0)」で問題なさそうです。
なおフォント列挙時には図23の※3の値の値が返されるそうですが、これ以上の情報は掴めませんでした。
出力品質(Q、lfQuality)は、「設定するフォントの属性と実フォントの属性をどの程度一致」させるかを指定するもので、下図の中から指定します。
(定数)内容
DEFAULT_QUALITY0フォントの文字品質は重視されない
DRAFT_QUALITY1フォントの文字品質は、PROOF_QUALITY を使用したときほどは重視されない
PROOF_QUALITY2フォントの文字品質が、論理フォントの属性を正確に一致させることよりも重視される
NONANTIALIASED_QUALITY3アンチエイリアス処理は一切行われない
ANTIALIASED_QUALITY4アンチエイリアスをサポートしているフォントのサイズが小さ過ぎたり大き過ぎる場合に、アンチエイリアス処理が実行される
CLEARTYPE_QUALITY5クリアタイプを利用してテキストを描画
図25

図25の内容を見ると、「文字品質」と「アンチエイリアス」という言葉が出てきます。そこで文字品質の悪いラスタフォントを使い、lfQuality値を振って描画してみたのが下図です。
ラスタフォントでlfQuality値を振って描画
図26

「PROOF_QUALITY(値=2)」だけが設定よりも文字が小さく、他はラスタフォント特有のギザギザが目立つ描画です。
PROOF_QUALITYは「設定フォントを一致させることよりも、文字品質を重視」しますので、サイズ(ここでは55ピクセル)を無視して文字の輪郭がスムーズに見えるまでサイズを縮めているものと思われます。
またアンチエイリアス(ギザギザの部分を中間色を使って目立たなくする)をサポートしているMSゴシックを使い、lfQuality値を振って描画してみたのが下図です。
MSゴシックでlfQuality値を振って描画
図27

良く見ると「NONANTIALIASED_QUALITY(値=3)」だけが、ギザギザが目立つ気がします。NONANTIALIASED_QUALITYは「アンチエイリアス処理をしない」という指定なので、ギザギザが残ったまま描画されたと思われます。
以上からlfQuality引数としては、「DEFAULT_QUALITY(値=0)」を指定しておけば大きな問題はなさそうです。
ピッチとファミリ(PAF、lfPitchAndFamily)は、「フォントのピッチ」「フォントファミリ」を指定します。
「ピッチ」は文字間の幅で、可変幅と固定幅があります。値としては図28から選択します。
固定幅は「どんな文字でも1文字1文字の幅は同じ」です。可変幅は「"i" や "l" のように横に広がりの少ない文字は文字幅を狭く」することで、見易い文字列にするものです。
ピッチ定数
(定数)内容
DEFAULT_PITCH0既定
FIXED_PITCH1固定幅
VARIABLE_PITCH2可変幅
図28

「ファミリ」はフォントの外観です。希望する正確な書体が入手できない場合にフォントを指定するためのもの との説明がされています。ファミリの値は図29から選択します。
ファミリ定数
(定数)内容
FF_DONTCARE0ファミリを指定せず。又はファミリ不明
FF_ROMAN16可変ストローク幅+セリフ(文字のひげ飾り)付きフォント(MSR Serif、Times Roman 等)
FF_SWISS 32可変ストローク幅+セリフ無しフォント(MS Sans Serif、Helvetica 等)
FF_MODERN48 固定ストローク幅+セリフ付き or セリフ無しフォント(Pica、Elite、Courier NewR等)
FF_SCRIPT 64手書き風フォント(Script、Cursive 等)
FF_DECORATIVE80装飾付きフォント(Old English 等)
図29

このlfPitchAndFamily引数を説明しているサイトでは「下位2ビットでピッチを、上位4ビットでファミリを指定」とあります。最初意味が良く分からなかったので色々試してみたところ「8ビットを上下に分け、その下位4ビット中の下2ビットでピッチを、上位4ビットでファミリを指定」という事のようです。合わせた8ビットは図30のような組み合わせになるかと思います。
ピッチとファミリの結合
図30

このピッチとファミリの組み合わせ値をlfPitchAndFamily引数に指定して文字を描画した結果が、図31です。なおフォント名(lfFaceName引数)には空文字を指定しています。
ピッチとファミリを振った時の描画文字
図31

図31からは以下のような事が分かります。
 ・全部で「4種類のフォント」が描画される
 ・ピッチ(固定幅か可変幅か)は指定した通りに文字描画される
 ・DEFAULT_PITCHは、ほとんどがVARIABLE_PITCHを指しているが、FF_MODERNの時のみFIXED_PITCHとなる
 ・FF_SWISS(値=32)はセリフ(ひげ飾り)無しとなっているのに、固定幅ではセリフ有りになる
 ・FF_SCRIPT(値=64)やFF_DECORATIVE(値=80)は、手書き風・装飾付きのフォントにはならない
なお、図31はフォント名を空文字指定した結果であり、フォント名を指定(例えば MSゴシック)すると全て同じフォントとなります。ですので「lfPitchAndFamily引数は、フォント名を指定しなかった時に、既定フォントを指定」するものなのかもしれません。
ですので既定フォントとする場合には、どのような文字外観が良いかを考えてlfPitchAndFamily引数を指定すべきと考えます。
なお、下位3桁目はTrueTypeフォント(TMPF_TRUETYPE = 4)の設定、下位4桁目は等幅フォント(MONO_FONT = 8)の設定のようですが、値を振ってみても描画文字は図31と変わりませんでしたので、無視しても良さそうです。
また1ビット目と2ビット目の両方にフラグが立つとき(例えば 0000 0011)には、2ビット目が有効になるようです。
フォント名(F、lfFaceName)は、文字通りフォント名を指定します。
但し指定する文字列には厳密さが求められ、半角・全角や大文字・小文字、スペース有無など一部でも異なると「無指定=既定のフォント」と判断されてしまうようです。
確実に指定する方法としては、以下のように「セルのフォント名をコピーし、コードに貼り付け」る手法です。今のところ、うまく動作しています。
フォントの文字列を正確に記す方法
図32

3.描画道具の選択と削除

描画道具であるペン・ブラシ・フォントのハンドルが準備できたら、描画する内容に対応した道具を「SelectObject関数」を使って選択します。また描画道具が不要になったら「DeleteObject関数」で、道具を削除します。
役割関数名宣言
引数引数の内容構造体戻り値
指定オブジェクトを選択SelectObjectDeclare PtrSafe Function SelectObject Lib "gdi32" _
(ByVal hDC As LongPtr, ByVal hObject As LongPtr) As LongPtr
hDc
hObject
DCのハンドル
描画道具オブジェクトのハンドル
成功=前Objハンドル
オブジェクトを削除し解放DeleteObjectDeclare PtrSafe Function DeleteObject Lib "gdi32" (ByVal hObject As LongPtr) As Long
hObject描画道具オブジェクトのハンドル成功=0以外
失敗=0
図33

3-1.描画道具の選択(SelectObject)

SelectObject関数は、描画道具を選択します。
第1引数(hDc)にはデバイスコンテキストのハンドルを指定します。
第2引数(hObject)には、作成した道具のハンドルを指定します。
SelectObject関数で戻るのは「今まで選択していた描画道具のハンドル」です。一番初めにSelectObjectを実行した時には「既定の道具のハンドル」となります。
図01の流れ図で示した「描画道具を元に戻す」場合には、この戻り値を「再度SelectObject関数に指定」することで実現できます。
寄り道(既定値)
SelectObject関数で道具を選択する前の「既定の道具」は、下記のように決まっているようです。
道具(定数)
ペンBLACK_PEN7
ブラシWHITE_BRUSH0
フォントSYSTEM_FONT13
図34

この既定の道具のみを使って描画したのが下記になります。
既定のペン・ブラシ・フォントで描画
図35

ちなみに、この既定の道具のハンドルを事前に得ることも可能です。下記の「GetStockObject関数」に図34の値を引数指定することで、ハンドルが取得できます。
役割関数名宣言
引数引数の内容構造体戻り値
既定値のハンドルを表示GetStockObjectDeclare PtrSafe Function GetStockObject Lib "gdi32" (ByVal nIndex As Long) As LongPtr
nIndex既定値成功=ハンドル
図36

このGetStockObject関数を活用することで、一通り作業が終了した時点で道具を初期状態に戻す事も可能です。

3-2.描画道具の削除(DeleteObject)

DeleteObject関数は、描画道具を削除します。第1引数(hObject)には、作成した道具のハンドルを指定します。
なお、削除後でも道具のハンドル番号(LongPtr型)は取得できるのですが、そのハンドルで再びSelectObjectしても「既定値での描画」となります(≒描画道具は死んでいる)。

4.描画作業

以下では、線・円形・矩形・点・文字の描画について紹介しますが、この描画に関するAPI関数は実に多くの種類があります。ここで示した関数はその一部ですので、あらかじめ御了承下さい。
他の関数について調べる場合は、検索サイトで「API」+「GDI」等と検索してみてください。

4-1.線の描画

4-1-1.単線

1本の単線を引くには、以下のような手順となります。
 描画する線の始点の位置に移動(MoveToEx関数) → 描画する線の終点を指定(LineTo関数)
なお線を引いた後は、LineTo関数に指定した終点が「現在の位置」に変わります。
役割関数名宣言
引数引数の内容構造体戻り値
指定した点を
新しい現在位置にする
MoveToExDeclare PtrSafe Function MoveToEx Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X As Long, ByVal Y As Long, lpPoint As POINTAPI) As Long
hDc
x, y
lpPoint
DCハンドル
新しい現在位置のxy座標
それまでの現在位置(POINTAPI構造体)
Type POINTAPI
 x As Long
 y As Long
End Type
成功=0以外
失敗=0
現在位置と指定終点を
結ぶ直線を描画
LineToDeclare PtrSafe Function LineTo Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X As Long, ByVal Y As Long) As Long
hDc
x, y
DCのハンドル
直線終点のxy座標
成功=0以外
失敗=0
図37

現在の位置を変更する「MoveToEx関数」の第1引数(hdc)には、デバイスコンテキスト(DC)のハンドルを指定します。
第2、第3引数(x, y)には、新しい位置(=移動先)の座標をピクセル単位で指定します。
第4引数(lpPoint)には、それまでの現在位置が戻されますので、あらかじめPOINTAPI構造体の変数を準備しておき、準備した変数を指定します。それまでの位置が不要であっても、POINTAPI構造体の変数を指定しないとエラーとなってしまいます。
始点まで移動したら「LineTo関数」で線を引きます。
第1引数(hdc)には、デバイスコンテキスト(DC)のハンドルを指定します。
第2、第3引数(x, y)には、終点の位置の座標をピクセル単位で指定します。
簡単な描画例でのコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種は既定で描画しています。
  1. '========== ⇩(1) 直線の描画 ============
  2. Sub Line_Draw1()
  3.  Dim P_old As POINTAPI     '←元の現在位置
  4.  Me.Repaint   '←描画準備
  5.  Call MoveToEx(hdc, 50, 50, P_old)   '←現在位置の変更
  6.  Call LineTo(hdc, 150 , 150)   '←終点に向かって線を描画
  7. End Sub
図38

04行目「Me.Repaint」は、描画の準備をしています。
06行目「Call MoveToEx(hdc, 50, 50, P_old)」では、現在の位置を(50, 50)の位置に移動させています。第4引数のP_oldには移動前の位置情報が入りますが、今回は特に使用していません。
08行目「Call LineTo(hdc, 150 , 150)」は、現在位置から(150, 150)の位置に向かって直線を引いています。
図38のコードで描画される線は、以下のようになります。
LineTo関数で描画する線
図39

線が引かれた後(08行目が完了した後)は、現在の位置は終点(今回の場合は(150, 150)の位置)に移動済みです。ですのでLineTo関数を繰り返し実行すれば、一筆書きのように折れ線や矩形が描画できます(但しあくまで直線の集まりなので、直線間で囲まれた部分がハッチングされる事はありません)。
寄り道(Repaintメソッドの役割)
「GDI描画をフォーム起動時に実施する場合は、その前にRepaintメソッドの実行が必要」と言われています。確かにActivateイベント内で描画を実行する時にはRepaintが無いと描画されません。
一般的にRepaintメソッドは「フォームを再描画」する機能と説明されます。しかし今回はGDIでの描画前にRepaintをしているので、この説明では納得できません。
そこで、「描画処理」と「Repaint」の位置について調べてみたのが図40です。
Repaintメソッドの実行位置
図40

Case1~3は、描画処理を実行する位置がInitializeイベント時、Activateイベント時、Activateイベントが完了した後 と分けています。
そして描画処理の前にRepaintメソッドをどこで実行するかが①~③の位置です。Initializeイベント内が①、Activateイベント内が②、Activateイベントが完了した後が③としました。
コードを組んで確かめた結果、
Case1(Initializeイベント内で描画処理)では、その直前(=Initializeイベント内)でRepaintメソッドを実行しても描画されませんでした。
Case2(Activateイベント内で描画処理)では、Repaintの実行位置はInitialize内とActivate内の2か所が考えられますが、Initializeイベント内でRepaintメソッドを実行しても描画されません。一方、Activateイベント内でRepaintメソッドを実行した時には描画されます。
Case3(Activateイベント完了後に描画処理)では、Repaintメソッドが有っても無くても描画されます。
Case2では、Repaintの位置で「DoEventsメソッド」を実行しても同じ結果(Activate内で実行すると描画される)となりますが、DoEventsを挟まないDo~LoopやSleep関数では描画されない事も分かりました。
現象をまとめると、フォームが表示された状態で、制御が一旦O/Sに移った後に描画処理をすると描画されるようです。つまりRepaintメソッドは「ユーザーフォーム上の制御をO/Sに一時的に移管」する役目もしている とも考えれらます。
なお「Waitメソッドで1秒以上待機」すると描画される現象も確認されたのですが、Waitの間はユーザーフォーム上にFocusが移ってきている訳でも無さそうなので、まだ裏に何かありそうです。
なお描画された後にRepaintを実行すると、当然ながら既に描かれた描画は消えますので「既存の描画を消去」≒「新たな描画の準備」と言う説明でも成り立ちそうです。

4-1-2.折れ線(複数の線の連続)

上記のLineTo関数を繰り返すことでも折れ線が描画できますが、1回の実行で折れ線を描画するのが下記です。またこの関数を使えば、MoveToEx→LineTo を使わず単線を描画することも可能です。
ここでは「Polyline」と「PolylineTo」の2種を紹介します。Polylineの方は描画後もペンの現在位置は変わりませんが、PolylineToの方は描画の終点にペンの現在位置が移動します。
役割関数名宣言
引数引数の内容構造体戻り値
折れ線を描画
(ペンの現在位置は不変)
PolylineDeclare PtrSafe Function Polyline Lib "gdi32" _
(ByVal hdc As LongPtr, lpPoint As POINTAPI, ByVal nCount As Long) As Long
hDc
lpPoint
nCount
DCハンドル
空間内の点の座標(POINTAPI構造体)
lpPoint配列内の点の数
Type POINTAPI
 x As Long
 y As Long
End Type
成功=0以外
失敗=0
折れ線を描画
(現在位置を始点として描画開始し
ペンの現在位置は終点に移動)
PolylineToDeclare PtrSafe Function PolylineTo Lib "gdi32" _
(ByVal hdc As LongPtr, lpPoint As POINTAPI, ByVal nCount As Long) As Long
hDc
lpPoint
nCount
DCハンドル
空間内の点の座標(POINTAPI構造体)
lpPoint配列内の点の数
Type POINTAPI
 x As Long
 y As Long
End Type
成功=0以外
失敗=0
図41

指定するパラメータは、どちらも3つです。
第1引数「hDc」には、デバイスコンテキスト(DC)のハンドルを指定します。
第2引数「lpPoint」は、各座標位置をPOINTAPI構造体に格納して指定します。
第3引数「nCount」には、lpPoint配列の要素数(=点の数)を指定します。
簡単な描画例でのコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種は既定で描画しています。
  1. '========== ⇩(2) 折れ線の描画 ============
  2. Sub Line_Draw2()
  3.  Dim P(1 To 4) As POINTAPI   '←点の位置の配列
  4.  Dim P_old As POINTAPI     '←元の現在位置
  5.  Call MoveToEx(hdc, 50, 50, P_old)   '←現在位置の変更
  6.  Me.Repaint   '←描画準備
  7.  P(1).X = 100: P(1).Y = 150
  8.  P(2).X = 150: P(2).Y = 50
  9.  P(3).X = 200: P(3).Y = 150
  10.  P(4).X = 250: P(4).Y = 50
  11.  Call Polyline(hdc, P(1), 4)
  12. ' Call PolylineTo(hdc, P(1), 4)
  13. End Sub
図42

22行目「Dim P(1 To 4) As POINTAPI」は、Polyline関数またはPolylineTo関数の第2引数に渡す「点の位置の配列」を宣言しています。
25行目「Call MoveToEx(hdc, 50, 50, P_old)」では、ペンの現在位置を変更しています。なおPolyline関数を使う時は、このMoveToExでの位置変更は無関係となります。
27行目「Me.Repaint」で描画の準備をしています。
29~32行目では、点の位置の配列にデータを入れています。配列変数Pのデータ型がPOINTAPI構造体(XとYの2要素)ですので、いわば「4 × 2」の2次元配列のような形となります。
34行目「Call Polyline(hdc, P(1), 4)」では、Polyline関数を使用して折れ線を引いています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定します。
第2引数は点の座標であるPOINTAPI構造体(ここでは配列変数 P(1 To 4) )を指定するのですが、「配列の1番目の要素」を指定するようです。
感覚的には「Call Polyline(hdc, P, 4)」のように、配列変数全体を指定するのが正しそうに思うのですが、それだとエラーが発生します。もちろん2番目の要素を指定する事も可能ですが、そうすると1番目の座標が無視されます。
第3引数には、配列の要素数を指定します。
図42のコードを実行したのが図43の左側です。なお見え消しにしてあるPolylineTo関数の35行目「Call PolylineTo(hdc, P(1), 4)」を実行したのが右側になります。
PolylineとPolylineToの実行結果
図43

Polyline関数では現在位置は無視され、配列内の座標位置データの間のみを直線で結びます。
一方PolylineTo関数は現在位置(50, 50)を始点とし、まず座標位置データ配列の最初の要素位置に線を引き、その後で配列内の各点の位置を結んでいきます。
描画後の現在位置は、Polyline関数については移動しませんが、PolylineTo関数は配列の最後の要素の位置に移動します。
なお、各関数の引数指定に失敗した例を下に示します。
要素数の設定ミスや位置データ欠落時の描画結果
図44

図44の左側は、点の位置座標の配列に「データが欠落」していた場合です。POINTAPI構造体のXY各要素はLong型なので、要素が空だと「ゼロ」とみなされます。ですので、その空の点は「デバイスコンテキストの描画領域の原点 (0, 0)」とみなして線で結んでしまうことになります。
図44の右側は、第3引数の「要素の数」が実際の配列の要素数よりも多かった場合です。Polyline関数等は第3引数に指定した個数だけ点を探しに行くので「指定した配列の外の値(配列データが並んでいるメモリー域の次の場所に書き込まれている値)」を拾ってきます。この値は全く予想がつかず、実際に試してみても「実行する度に異なる線」が引かれてしまいます。
この対策としては要素数を直値で指定せず、以下のような計算式で指定すれば間違いは減るかと思われます。
 Call Polyline(hdc, P(1), UBound(P, 1) - LBound(P, 1) + 1)

4-2.円形関連の描画

楕円形や弓形、扇形を作成するAPI関数は以下になります。
役割関数名宣言
引数引数の内容構造体戻り値
楕円を描画EllipseDeclare PtrSafe Function Ellipse Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long
hDc
X1, Y1
X2, Y2
DCハンドル
楕円に外接する長方形の左上隅のxy座標
楕円に外接する長方形の右下隅のxy座標
成功=0以外
失敗=0
弓形を描画ChordDeclare PtrSafe Function Chord Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long, _
ByVal X3 As Long, ByVal Y3 As Long, ByVal X4 As Long, ByVal Y4 As Long) As Long
hDc
X1, Y1
X2, Y2
X3, Y3
X4, Y4
DCハンドル
楕円に外接する長方形の左上隅のxy座標
楕円に外接する長方形の右下隅のxy座標
楕円の弧の始点(放射直線の端点)のxy座標
楕円の弧の終点(放射直線の端点)のxy座標
成功=0以外
失敗=0
扇形を描画
A側からB側に
反時計回りに
扇を作成
PieDeclare PtrSafe Function Pie Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long, _
ByVal X3 As Long, ByVal Y3 As Long, ByVal X4 As Long, ByVal Y4 As Long) As Long
hDc
X1, Y1
X2, Y2
X3, Y3
X4, Y4
DCハンドル
楕円に外接する長方形の左上隅のxy座標
楕円に外接する長方形の右下隅のxy座標
最初の放射直線の端点のxy座標(A)
2番目の放射直線の端点のxy座標(B)
成功=0以外
失敗=0
図45

4-2-1.楕円(Ellipse関数)

Ellipse関数の宣言文を再掲します。
Declare PtrSafe Function Ellipse Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long

楕円は図46の左側のように「2つの焦点に長い1本の糸の輪を引っ掛け、輪の中に筆記具を入れて糸が張る状態で筆記具を動かす」事によってできる図形です。VBAでは「楕円に外接する長方形」を指定することで楕円形が作成されます。
なお2つの焦点が1つに重なったものが円ですので、円は楕円の一部ということになります。その際には、外接する長方形は「正方形」となります。
楕円の作り方とVBAでの定義の仕方
図46

Ellipseは、この楕円形を描画する関数です。第2~4引数には図46右側のX1,Y1,X2,Y2を指定することで長方形に内接する楕円形が描画されます。
簡単な描画例でのコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種・ブラシ種は既定で描画しています。
  1. '========== ⇩(3) 楕円形の描画 ============
  2. Sub Ellipse_Drow1()
  3.  Me.Repaint
  4.  Call Ellipse(hdc, 50, 50, 150, 150)
  5. ' Call Ellipse(hdc, 50, 50, 250, 150)
  6. End Sub
図47

45行目「Call Ellipse(hdc, 50, 50, 150, 150)」で楕円形を作成しています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数には、外接する長方形の左上隅のXY座標位置(ピクセル単位)を指定(ここでは(50, 50) )します。
第4~5引数には、外接する長方形の右下隅のXY座標位置を指定(ここでは(150, 150) )します。
作成された楕円形は、図48のようになります。図47では正方形を指定していますので、左側のような円となりますが、見え消しの46行目「Call Ellipse(hdc, 50, 50, 250, 150)」のように長方形を指定すれば、右側のような楕円形となります。

Ellipse関数での楕円形の描画
図48

また、楕円に外接する長方形の指定方法(X1Y1、X2Y2)ですが、説明では「左上隅がX1Y1」「右下隅がX2Y2」となっています。しかしX1Y1とX2Y2を入れ替えても、また右上隅と左下隅を指定しても、図49のように同じ楕円形が描画されます。
つまり「長方形の四隅の、対角となる隅の座標を指定」することで長方形は描画可能ということになります。
この「入れ替え可」はこの関数のみの現象ではなく、Pie関数、Chord関数でも可能です。しかも扇形や弓形が上下・左右で逆になる等の副作用もなさそうです。
また円形関数だけではなく、矩形描画の関数でも入れ替え可能です。
Pie関数で楕円を囲む四角の頂点の指定方法
図49

尚この入れ替えは、後からコードを見た人が混乱しますのでお勧めはしませんが、やむなく入れ替えなければならない時には有難い仕様かもしれません。

4-2-2.弓形(Chord関数)

Chord関数の宣言文を再掲します。
Declare PtrSafe Function Chord Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long, _
ByVal X3 As Long, ByVal Y3 As Long, ByVal X4 As Long, ByVal Y4 As Long) As Long

弓形とは「楕円の一部を切り取り、切り取った部分を直線で結んだ図形」です。
簡単な描画例のコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種・ブラシ種は既定で描画しています。
  1. '========== ⇩(4) 弓形の描画 ============
  2. Sub Chord_Drow1()
  3.  Me.Repaint
  4.  Call Chord(hdc, 50, 50, 150, 150, 120, 110, 80, 180)
  5. ' Call Chord(hdc, 50, 50, 150, 150, 80, 180, 120, 110)
  6. ' Call Chord(hdc, 50, 50, 150, 150, 100, 100, 80, 180)
  7. End Sub
図50

65行目「Call Chord(hdc, 50, 50, 150, 150, 120, 110, 80, 180)」で弓形を作成しています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数には、外接する長方形の左上隅のXY座標位置(ピクセル単位)を指定します。
第4~5引数には、外接する長方形の右下隅のXY座標位置を指定します。
この第2~5引数のデータで、作られた長方形に内接する楕円形の形が決定します。
第6~7引数は、楕円中心からの直線の「1番目の端点」のXY座標を指定します。
第8~9引数は、楕円中心からの直線の「2番目の端点」のXY座標を指定します。
第6引数以降が何を指しているのか、説明文からでは分かり難いので、描画した弓形で説明をします。
図50のコードの結果が、図51の一番左側の図になります。
Chord関数で弓形を描画する
図51

楕円の中心(=四角の中心)から、第6~7引数の座標(X3, Y3)の点に向かって、楕円形の外周に交差するまで直線を引きます。なお座標(X3, Y3)の点が楕円の内側にあっても、その点を越えて楕円形の外周に交差するまで直線を引きます。この直線と楕円の外周が交差する点が「弓形の円弧の始点」となります。
また楕円の中心から、第8~9引数の座標(X4, Y4)の点に向かって、楕円形の外周に交差するまで直線を引きます。この直線と楕円の外周が交差する点が「弓形の円弧の終点」となります。
始点と終点が求まりました。ではどの方向に円弧を作るかですが、始点から終点に向かって「反時計回り」に円弧が作られ、残された部分が直線で結ばれます。
「反時計回りの円弧」ですので、見え消しの66行目「Call Chord(hdc, 50, 50, 150, 150, 80, 180, 120, 110)」のように端点の入れ替え(X3,Y3 ⇔ X4,Y4)をすると、図51の中央図のように「左図の残り」のような形となります。
また、見え消し67行目「Call Chord(hdc, 50, 50, 150, 150, 100, 100, 80, 180)」のように端点が「楕円の中心」となる場合、その端点に対しては「X方向の値のみを増やす」ような処理をしているのか、中心から直線を楕円右側に平行に引く事になります。結果は図51の右図のようになります。
また両方の端点が中心となる場合は「完全な楕円」となり、直線部分は無くなります。

4-2-3.扇形(Pie関数)

Pie関数の宣言文を再掲します。
Declare PtrSafe Function Pie Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long, _
ByVal X3 As Long, ByVal Y3 As Long, ByVal X4 As Long, ByVal Y4 As Long) As Long

扇形とは「2本の半径と、その間にある円弧によって囲まれた図形」です。
簡単な描画例のコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種・ブラシ種は既定で描画しています。
  1. '========== ⇩(5) 扇形の描画 ============
  2. Sub Pie_Drow1()
  3.  Me.Repaint
  4.  Call Pie(hdc, 50, 50, 150, 150, 120, 110, 80, 180)
  5. ' Call Pie(hdc, 50, 50, 150, 150, 80, 180, 120, 110)
  6. ' Call Pie(hdc, 50, 50, 150, 150, 100, 100, 80, 180)
  7. End Sub
図52

85行目「Call Pie(hdc, 50, 50, 150, 150, 120, 110, 80, 180)」でPie関数を呼び出しています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数は、楕円形に外接する長方形の左上隅のXY座標(ピクセル単位)を指定します。
第4~5引数は、楕円形に外接する長方形の右下隅のXY座標を指定します。
第6~7引数は、1番目の直線の端点のXY座標を指定します。
第8~9引数は、2番目の直線の端点のXY座標を指定します。
第6引数以降について、描画した扇形で説明をします。内容は弓形(Chord関数)とほぼ一緒です。
図52のコードの結果が、図53の一番左側の図になります。
Pie関数での扇形の描画
図53

まず、第2~5引数の値で決定する「長方形(図53の赤点線の四角)」の内側に接する楕円形が決まります。
次に楕円の中心(=四角の中心)から、第6~7引数の座標(X3, Y3)の点に向かって、楕円形の外周に交差するまで直線を引きます。なお座標(X3, Y3)の点が楕円の内側にあっても、その点を越えて楕円形の外周に交差するまで直線を引きます。この直線が「扇形の1本目の半径」となります。
また楕円の中心(=四角の中心)から、第8~9引数の座標(X4, Y4)の点に向かって、楕円形の外周に交差するまで直線を引きます。この直線が「扇形の2本目の半径」となります。
2本の半径のどちら側に円弧を作るかですが、1本目の半径から2本目の半径に向かって「反時計回り」に円弧が作られます。
見え消し86行目「Call Pie(hdc, 50, 50, 150, 150, 80, 180, 120, 110)」のように、1本目の半径と2本目の半径が入れ替わると、図53の中央のように「左図の残り」のような形となります。
また、見え消し87行目「Call Pie(hdc, 50, 50, 150, 150, 100, 100, 80, 180)」のように、直線の端点(第6~9引数)のどちらかが「楕円の中心」となる場合は、図53の右図のように「中心からの半径を楕円右側に平行に引く」事になります。
そして両方の端点とも楕円の中心となる場合は、完全な楕円となりますが、楕円の右側に半径が2つ重なる形となり「直線は見えてしまう」ので注意が必要です。

4-3.矩形関係の描画

矩形(四角形や多角形)を描画するAPI関数は以下になります。
役割関数名宣言
引数引数の内容構造体戻り値
長方形を描画RectangleDeclare PtrSafe Function Rectangle Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long
hdc
X1, Y1
X2, Y2
DCハンドル
長方形の左上隅のxy座標
長方形の右下隅のxy座標
成功=0以外
失敗=0
多角形を描画PolygonDeclare PtrSafe Function Polygon Lib "gdi32" _
(ByVal hdc As LongPtr, lpPoint As POINTAPI, ByVal nCount As Long) As Long
hdc
lpPoint
nCount
DCハンドル
多角形の各頂点の座標(POINTAPI構造体)
配列内の各多角形の頂点の数
Type POINTAPI
 x As Long
 y As Long
End Type
成功=0以外
失敗=0
図54

4-3-1.長方形の描画(Rectangle)

長方形の描画にはRectangle関数を使用します。そのAPI関数宣言文を再掲します。
Declare PtrSafe Function Rectangle Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long

ここでいう長方形は「各辺が水平または垂直の四角形」ですので、長方形の対角2点の座標を指定すれば一意に形が決まることになります。以下では、簡単な描画例でのコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種・ブラシ種は既定で描画しています。
  1. '========== ⇩(6) 長方形の描画 ============
  2. Sub Rectangle_Draw1()
  3.  Me.Repaint
  4.  Call Rectangle(hdc, 50, 50, 150, 150)
  5. ' Call Rectangle(hdc, 50, 50, 250, 150)
  6. End Sub
図55

105行目「Call Rectangle(hdc, 50, 50, 150, 150)」では、Rectangle関数を呼び出し長方形を描画しています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数は、長方形の左上隅のXY座標(ピクセル単位)を指定します。
第4~5引数は、長方形の右下隅のXY座標を指定します。
図55のコードの結果が、図56の左側になります。
Rectangle関数での長方形の描画
図56

長方形の左上隅・右下隅の座標を指定することで長方形が描画されることが分かります。図56の左側は正方形となりましたが、見え消しの106行目「Call Rectangle(hdc, 50, 50, 250, 150)」を使用すれば図56の右側のように長方形になります。
なお図49の所でも説明したように、長方形を描画するための隅は「対角であればどこの隅の座標を指定しても、同じ長方形」が描画されます。

4-3-2.多角形の描画(Polygon)

Polygon関数のAPI宣言文を再掲します。
Declare PtrSafe Function Polygon Lib "gdi32" _
(ByVal hdc As LongPtr, lpPoint As POINTAPI, ByVal nCount As Long) As Long

多角形は、頂点の座標位置の配列をPolygon関数に指定する事で描画できます。以下では簡単な描画例でのコードを紹介します。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、ペン種・ブラシ種は既定で描画しています。
  1. '========== ⇩(7) 多角形の描画1 ============
  2. Sub Polygon_Draw1()
  3.  Dim p(1 To 5) As POINTAPI
  4.  Me.Repaint
  5.  p(1).x = 50:  p(1).y = 50
  6.  p(2).x = 50:  p(2).y = 150
  7.  p(3).x = 250:  p(3).y = 150
  8.  p(4).x = 250:  p(4).y = 50
  9.  p(5).x = 150:  p(5).y = 20
  10.  Call Polygon(hdc, p(1), 5)
  11. End Sub
図57

122行目「Dim p(1 To 5) As POINTAPI」で、POINTAPI構造体としての配列変数を宣言します。
126~130行目では、5つの座標位置の値を配列にしています。
132行目「Call Polygon(hdc, p(1), 5)」では、Polygon関数で多角形を描画しています。
第1引数にはデバイスコンテキスト(DC)のハンドルを指定し、第2引数には座標位置を格納したPOINTAPI構造体の配列の「先頭要素」を指定します。
第3引数には「POINTAPI構造体の配列の要素数(=多角形の頂点の数)」を指定します。
図57のコードにより描画された図形が図58の左側になります。
配列のP(1)からP(5)までを直線で結び、最後の座標から先頭の座標に対しても線で結ぶことで、閉じた多角形となります。閉じられた多角形の内側は「ブラシで塗りつぶし」されます。
Polygon関数で作るの多角形の描画
図58

図57で作成したPOINTAPI構造体の配列の要素数が5つだったので、図58の左側は「五角形」となりました。
一方、下の図59のコードで作った多角形が図58の右側です。各頂点の座標は全く同じですが「配列の順番」が異なることで星形の図形を描画することも可能です。
  1. '========== ⇩(8) 多角形の描画2 ============
  2. Sub Polygon_Draw2()
  3.  Dim p(1 To 5) As POINTAPI
  4.  Me.Repaint
  5.  p(1).x = 50:  p(1).y = 50
  6.  p(2).x = 250: p(2).y = 50    図57では P(4)
  7.  p(3).x = 50:  p(3).y = 150    図57では P(2)
  8.  p(4).x = 150: p(4).y = 20    図57では P(5)
  9.  p(5).x = 250: p(5).y = 150    図57では P(3)
  10.  Call Polygon(hdc, p(1), 5)
  11. End Sub
図59

なおPolygon関数で描画された多角形は、内部がブラシで塗りつぶされる(既定だと白色)のですが、図58の右側の星形図形では星形の先端領域だけがブラシで塗りつぶされ、中央部分は塗りつぶしされていない状態になります。
寄り道(多角形の塗りつぶし)
多角形を描画すると、図60のように「塗りつぶされない部位」が出てくる可能性があります。
GDIの、この塗りつぶしルールについては「多角形の各走査行で多角形の奇数番号の辺から偶数番号の辺までの間の領域を塗りつぶす」との説明がありますが、良く分からないので適当に解釈すると「各頂点を結ぶ辺に囲まれた領域を塗りつぶす」という事のようです。つまり頂点が含まれる部分だけを塗りつぶすという事でしょうか。
しかし図60の中央図のように、星形図形の頂点(P(4))の座標位置を少し下げた図形では「頂点P(4)が含まれる部分でも塗り潰されない」という場合もありますし、また一番右側のように、線を行ったり来たりさせた図形では「頂点を含まない部分でも塗り潰される」という場合もあります。
多角形内部が塗りつぶされない場合
図60

塗りつぶしルールについては上記以上の詳しい説明は見当たりません。しかし図60を見る限り、
 ・頂点とは、POINTAPI配列の各点では無く「全てを塗り潰した時でも、頂点となる点」
 ・辺によって分割された領域は、交互に塗りつぶす(図62の「ALTERNATEモード」にも通じる)
というルールが隠れているのでは? と推測しています。
一方、塗り潰されていないのは「塗りつぶしモード=ALTERNATE(交互)」となっている為です。この塗りつぶしモードを変更するには、SetPolyFillMode関数を使用します。
役割関数名宣言
引数引数の内容構造体戻り値
多角形の
塗りつぶしモード設定
SetPolyFillModeDeclare PtrSafe Function SetPolyFillMode Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal nPolyFillMode As Long) As Long
hdc
nPolyFillMode
DCハンドル
塗りつぶしモード値
成功=以前のモード値
失敗=0
図61

SetPolyFillMode関数の第1引数にはデバイスコンテキストのハンドルを指定します。そして第2引数には、下表(図62)の値を指定します。
(定数)内容
ALTERNATE1交互モード(既定)
多角形の内部で隣接しない部分を塗りつぶす
WINDING2全域モード
多角形の交差する内面も塗りつぶす
図62

SetPolyFillMode関数で「WINDINGモード」に変更した後にPolygon関数を実行する事で、「ALTERNATEモード」では塗りつぶされなかった領域が図63のように塗りつぶされるようになります。
WINDINGモードで多角形内部が塗りつぶされた状況
図63

但し図63の一番右の「頂点4の領域」だけは塗りつぶされません。
Microsoftの説明では「交差する線の方向が逆の場合は塗りつぶされない」とあるので、下図のような「わざと中庭を作るような図形(図64の一番右)」の場合は塗りつぶされないとは思うのですが、図63の一番右の場合はそれにも当てはまらず、理屈は分かりません。
WINDINGモードでも多角形内部が塗りつぶされない条件
図64

以上のように、SetPolyFillMode関数を使っても塗りつぶされない図形が存在するため、事前チェックなどが必要です。

4-4.点の描画

点は文字通り「描画できる最小単位」で、1ピクセルです。点を描画するにはSetPixel関数を使用します。
役割関数名宣言
引数引数の内容構造体戻り値
点の描画SetPixelDeclare PtrSafe Function SetPixel Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal x As Long, ByVal y As Long, ByVal crColor As Long) As Long
hdc
X, Y
crColor
DCハンドル
xy座標
成功=描画色のRGB値
失敗=-1
図65

SetPixel関数の第1引数には、デバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数には、点を描画するxy座標の位置(ピクセル単位)を指定します。
第4引数には点の色をRGB関数等を使って指定します。
以下で簡単な描画例のコードを紹介します。なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとしています。
  1. '========== ⇩(9) 点の描画1 ============
  2. Sub SetPixel_Draw1()
  3.  Me.Repaint
  4.  Call SetPixel(hdc, 50, 50, RGB(255, 0, 0))
  5. End Sub
図66

165行目「Call SetPixel(hdc, 50, 50, RGB(255, 0, 0))」では、(X=50, Y=50) の位置の1ピクセルを赤色にしています。
この図66のコードで点を描画したものが、図67の一番左です。
SetPixelでの点の描画結果
図67

1ピクセルは非常に小さく、目を凝らしてやっと見える程度です。アプリとしてユーザーフォーム上に「点を表示したい」といった場合にはもう少し大きくする事も必要です。
そこで狙った座標(今回は (50, 50) の位置)の上下左右にドットを増やしたのが、図67の中央図(3 x 3 = 9 ピクセル)、右図(5 x 5 =25 ピクセル)です。コードとしては、以下(図68)のようになります。
  1. '========== ⇩(10) 点の描画2 ============
  2. Sub SetPixel_Draw2()
  3.  Dim i As Integer
  4.  Dim j As Integer
  5.  Dim Pwide As Integer   '←点の大きさの係数
  6.  Pwide = 1    '← 1=3×3ピクセル 、2=5×5ピクセル
  7.  Me.Repaint
  8.  For i = -1 * Pwide To Pwide
  9.   For j = -1 * Pwide To Pwide
  10.    Call SetPixel(hdc, 50 + i, 50 + j, RGB(255, 0, 0))
  11.   Next j
  12.  Next i
  13. End Sub
図68

186行目「Pwide = 1」では、狙った点の周りに「何周の点を盛るか」を指定しています。ここでは「1周」です。
190~194行目では、For~Nextを二重にして点の周りに点を追加しています。これなら「点」と認識できそうです。
なお186行目を「Pwide = 2」にしたのが図67の右図となります。
またSetPixel関数は点を描画するだけで無く、工夫すれば点だけで下図のようなものも描画できます。左側はSINカーブで、中央と右側は円です。波形の数式さえ分かれば描画できることになります。
SetPixelでの点の描画結果
図69

SINカーブを描画するコードは以下になります。1回転=2π(3.14 × 2)ですので、5回転分(314/10)を描画している事になりますが、最後の部分はユーザーフォームの外になってしまっています。また波形の変化が少し大きいので、点線のような描画となってしまいます。
  1. '========== ⇩(11) 点の描画3(SINカーブ) ============
  2. Sub SetPixel_Draw3()
  3.  Dim i As Integer
  4.  Me.Repaint
  5.  For i = 0 To 314
  6.   Call SetPixel(hdc, i, 50 * Sin(i / 10) + 100, RGB(0, 0, 0))
  7.  Next i
  8. End Sub
図70

207行目「Call SetPixel(hdc, i, 50 * Sin(i / 10) + 100, RGB(0, 0, 0))」がSINカーブの式となりますが、フォーム内に収めるために「50 *」と「+ 100」とでSINカーブの形を整えています。
円形を描画(図69の中央)するコードが以下です。半径は50ピクセルとし、2πラジアン(=360度)を100等分して描画をしています。
  1. '========== ⇩(12) 点の描画4(円形) ============
  2. Sub SetPixel_Draw4()
  3.  Dim r As Integer
  4.  Dim s As Integer
  5.  r = 50
  6.  Me.Repaint
  7.  For s = 0 To 2 * 3.14 * 100
  8.   Call SetPixel(hdc, r * Cos(s / 100) + 100, r * Sin(s / 100) + 100, RGB(0, 0, 0))
  9.  Next s
  10. ' For s = 0 To 2 * 3.14 * 10
  11. '  Call SetPixel(hdc, R * Cos(s / 10) + 100, R * Sin(s / 10) + 100, RGB(0, 0, 0))
  12. ' Next s
  13. End Sub
図71

225行目「r = 50」は、円の半径を50ピクセルに指定しています。
228行目「For s = 0 To 2 * 3.14 * 100」は、2πラジアン(=1周360度)を100倍して描画しています。
229行目「Call SetPixel(hdc, r * Cos(s / 100) + 100, r * Sin(s / 100) + 100, RGB(0, 0, 0))」の中の(s / 100)は、カウンタ変数sを100倍しているので、XY位置としては1/100にしています。
試しに、100倍を10倍に減らす(見え消しにしている232~234行目のFor~Next文)と、描画される円は図69の一番右側のように「粗い点線の円」となります。
絵でも図でも結局はピクセルの集まりですから「SetPixel関数を使えば何でも描画できる」とも言えます。しかし後からの修正の手間や可読性を考えて、使えるGDI関数が存在するのであれば、使うべきだとは思います。

4-5.文字の描画

文字を描画する関数としては「TextOut関数」「DrawText関数」等があります。また、その文字の「文字色の設定」「背景の設定」の関数等があります。
役割関数名宣言
引数引数の内容構造体戻り値
文字を描画TextOutDeclare PtrSafe Function TextOut Lib "gdi32" Alias "TextOutA" _
(ByVal hdc As LongPtr, ByVal x As Long, ByVal y As Long, ByVal lpString As String, ByVal nCount As Long) As Long
hdc
x, y
lpString
nCount
DCハンドル
基準点のxy座標
描画する文字列
文字数
成功=0以外
失敗=0
文字を
四角内に描画
DrawTextDeclare PtrSafe Function DrawText Lib "user32" Alias "DrawTextA" _
(ByVal hdc As LongPtr, ByVal lpStr As String, ByVal nCount As Long, lpRect As RECT, ByVal wFormat As Long) As Long
hdc
lpStr
nCount
lpRect
wFormat
DCハンドル
描画する文字列
文字数
長方形領域座標(RECT構造体)
テキスト整形方法
Type RECT
 Left As Long
 Top As Long
 Right As Long
 Bottom As Long
End Type
成功=描画テキストの高さ 等
 (wFormat値により異なる)
失敗=0
文字の色SetTextColorDeclare PtrSafe Function SetTextColor Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal crColor As Long) As Long
hdc
crColor
DCハンドル
文字色
成功=旧文字色
失敗=CLR_INVALID値
文字の
背景色設定
SetBkColorDeclare PtrSafe Function SetBkColor Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal crColor As Long) As Long
hdc
crColor
DCハンドル
背景色
成功=旧背景色
失敗=CLR_INVALID値
文字の
背景モード設定
(透明/非透明)
SetBkModeDeclare PtrSafe Function SetBkMode Lib "gdi32" _
(ByVal hdc As LongPtr, ByVal nBkMode As Long) As Long
hdc
nBkMode
DCハンドル
背景モード
成功=旧背景モード
失敗=0
図72

4-5-1.文字を描画(TextOut関数)

TextOut関数は単純に文字を描画するものです。API宣言文を再掲します。
Declare PtrSafe Function TextOut Lib "gdi32" Alias "TextOutA" _
(ByVal hdc As LongPtr, ByVal x As Long, ByVal y As Long, ByVal lpString As String, ByVal nCount As Long) As Long

TextOut関数は、第1引数にデバイスコンテキスト(DC)のハンドルを指定します。
第2~3引数には、描画する文字の左上角のxy座標位置(ピクセル単位)を指定します。
第4引数には、描画する文字列を指定します。
第5引数には、描画する文字数を指定します。この文字数は「半角=1文字」「全角=2文字」という数え方になります。
以下で簡単な描画例のコードを紹介します。なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとし、文字フォントは既定としています。
  1. '========== ⇩(13) 文字の描画1(TextOut関数) ============
  2. Sub TextOut_Draw1()
  3.  Me.Repaint
  4.  Call TextOut(hdc, 50, 50, "Win32APIへようこそ", 18)
  5. End Sub
図73

244行目「Call TextOut(hdc, 50, 50, "Win32APIへようこそ", 18)」でTextOut関数を呼び出し、文字を描画しています。
第1引数はユーザーフォームのデバイスコンテキスト(DC)ハンドルを指定しています。
第2~3引数では、座標(50, 50)の位置を指定します。描画される文字の左上角がこの位置になります。
第4引数「"Win32APIへようこそ"」が描画する文字列になります。
第5引数には、第4引数に指定した文字列の文字数を「半角=1文字、全角=2文字」として指定します。今回の場合は「半角8文字+全角5文字」なので「18」となります。
図73のコードで描画される文字は図74の左側です。
TextOut関数での文字描画
図74

この第5引数の描画文字数を間違えると、表示もおかしくなります。
図74の中央図は、指定文字数が少なかった時です。例えば第5引数=13とすると「"う" の途中で13文字」に達してしまいますので、「"う" の字が記号」に化けてしまいます。(なお、例えば文字数を12と指定した時は「Win32APIへよ」で丁度切れるので、変な記号は出ません。)
一方図74の右側は、指定文字数が多かった時の例です。メモリー上に収まっている文字列の「次のメモリー位置にある値」を文字として表示してしまうために記号となってしまいます。
文字数を正しく指定するには数式を使うのが有効です。
まずVBAにはLen関数があります。しかし、この関数は単純に「半角でも全角でも、1文字は1」と計算するので、今回描画する「"Win32APIへようこそ"」では「13文字」となります。
一方LenB関数は文字列のバイト数を求めるものですが、VBA内ではUnicodeで文字を扱っているため「半角も全角も全て2バイト」ですので、単純にLen関数の倍の「26」という値となってしまいます。
そこで「半角=1文字、全角=2文字」と数えるコードを図75に示します。
  1. '========== ⇩(14) 文字の描画2(文字数の自動カウント付き) ============
  2. Sub TextOut_Draw2()
  3.  Dim str As String   '←表示する文字列
  4.  Dim n As Integer   '←文字数
  5.  str = "Win32APIへようこそ"
  6.  n = LenB(StrConv(str, vbFromUnicode))
  7. ' n = Evaluate("LenB(""" & str & """)")
  8.  Me.Repaint
  9.  Call TextOut(hdc, 50, 50, str, n)
  10. End Sub
図75

265行目「str = "Win32APIへようこそ"」で、描画文字を変数strに代入します。
266行目「n = LenB(StrConv(str, vbFromUnicode))」では、「半角=1文字、全角=2文字」で文字数を数えています。
右辺の内、StrConv関数は第1引数(今回は変数str)の文字列を変換するもので、変換する種類を第2引数に指定します。第2引数は今回「vbFromUnicode」で、これは「Unicodeからシステム既定コード(≒Shift-JIS)」への変換を意味します。
文字種を変換した後にLenB関数で文字数をカウントしています。
ちなみにセル上でワークシート関数を使い「=LenB("Win32APIへようこそ")」とすると、ちゃんと「半角=1文字、全角=2文字」として計算をしてくれます。ですので、ワークシート上の数式をVBA上で再現するEvaluateメソッドを使用し、見え消しの267行目「n = Evaluate("LenB(""" & str & """)")」としても良いです。
なおTextOut関数での文字の描画では、指定文字列に改行マーク(vbCrLfなど)を入れても、改行はしてくれません。改行が必要な場合はフォームの幅サイズを考えながら1行分の文字を描画し、フォント高さを考慮して描画位置を下に移動してから次の行を描画 のような作業が必要になります。なお次に説明するDrawText関数では、改行やTABも可能です。

4-5-2.文字を描画(DrawText関数)

DrawText関数では、TextOutよりも複雑な文字描画が可能です。API宣言文を再掲します。
Declare PtrSafe Function DrawText Lib "user32" Alias "DrawTextA" _
(ByVal hdc As LongPtr, ByVal lpStr As String, ByVal nCount As Long, lpRect As RECT, ByVal wFormat As Long) As Long

DrawText関数は、第5引数「wFormat(テキスト整形方法)」の指定の仕方で様々な描画が可能な関数です。例えば複数行の文字を一度に描画することも可能ですし、描画に必要充分な幅・高さを取得することも出来ます。
なおDrawText関数だけは、他の関数と異なりファイル名が「user32」です。なので本当はGDIでは無いのかもしれませんが、同じ仲間という事にして説明します。
DrawText関数には、5つの引数を指定します。
第1引数「hdc」にデバイスコンテキスト(DC)のハンドルを指定します。
第2引数「lpStr」には、描画する文字列を指定します。
第3引数「nCount」には、描画する文字数を指定します。この文字数はTextOut関数と同様「半角=1文字」「全角=2文字」という数え方になります。
第4引数「lpRect」には、文字を描画する範囲をRECT構造体で指定します。RECT構造体は、図76のように描画領域の左上角である「Left,Top」、および右下角である「Right,Bottom」の位置をピクセル単位で指定します。
RECT構造体に指定する描画領域
図76

第5引数「wFormat」にはテキスト整形方法を以下(図77)より指定します。項目値を和算する事で、複数指定も可能です。
(定数)内容
DT_TOP, DT_LEFT0テキストを上揃え+左揃え
DT_CENTER1テキストを水平方向に中央揃え
DT_RIGHT2 テキストを右揃え
DT_VCENTER4テキストを垂直方向に中央揃え
注:DT_SINGLELINE(値=32)と同時指定要
DT_BOTTOM8テキストを下揃え
注:DT_SINGLELINE(値=32)と同時指定要
DT_WORDBREAK16
(&H10)
スペースで区切られた単語は、描画領域の端をオーバーすると改行される。「vbCrLf」でも改行される。
DT_SINGLELINE32
(&H20)
スペースでも「vbCrLf」でも改行せず、1行で描画される。
DT_EXPANDTABS64
(&H40)
「vbTab」によりタブを挿入。タブの既定文字数は8文字。
注:DT_WORD_ELLIPSIS、DT_PATH_ELLIPSIS、DT_END_ELLIPSISとの同時指定不可
DT_TABSTOP128
(&H80)
9~16ビット目にタブの文字数を指定。既定(0を指定)は8文字。
注:DT_EXPANDTABSを同時指定しないとタブとして表示されない。またDT_CALCRECT、DT_EXTERNALLEADING、DT_INTERNAL、DT_NOCLIP、DT_NOPREFIXとの同時指定不可。
DT_NOCLIP256
(&H100)
クリッピングなし(指定枠を無視)で描画(≒TextOut関数)
DT_EXTERNALLEADING512
(&H200)
行間にフォントの外部レディングを含める
DT_CALCRECT1024
(&H400)
描画はせず、渡した文字の描画に必要充分な描画領域をRECT構造体に戻す。
領域の起点(左上角)は、指定した左上角(RECTのLeft値×Top値)が使われる。(空のRECTを渡すと、デバイスコンテキストの原点が起点となる)
DT_NOPREFIX2048
(&H800)
プレフィックス文字(&印)での処理を無効化し、1つの文字として認識し出力する。
DT_INTERNAL4096
(&H1000)
システムフォント(既定)時のFont高さを戻す。
(これ以外は、指定フォントでの高さを戻す)
DT_EDITCONTROL8192
(&H2000)
部分的に表示される最後の行を表示しない
DT_PATH_ELLIPSIS16384
(&H4000)
フォルダー名の様に「¥印」で区切られた文字列が対象。
入りきらないフォルダー名が省略印になり、末尾のファイル名が完全に出力される。
(「DT_MODIFYSTRINGの同時指定要」だが、現時点では指定しなくても省略記号となる)
DT_END_ELLIPSIS32768
(&H8000)
ファイル名のような「¥印」で区切られた文字列も、半角スペースで区切られた単語も対象。
文字列末尾が領域右側に収まらない場合、末尾が省略記号に置き換わる。
末尾でない単語が領域右側を超える場合は、省略記号無く切り捨てられる
(「DT_MODIFYSTRINGの同時指定要」だが、現時点では指定しなくても省略記号となる)
DT_MODIFYSTRING65536
(&H10000)
指定文字列を加工条件に従って変更する。
(DT_END_ELLIPSISまたはDT_PATH_ELLIPSISと同時指定される以外は無効)
DT_RTLREADING131072
(&H20000)
フォントがヘブライ語またはアラビア語の場合、右から左への読み取り順序にする
(日本語では特に変化なし)
DT_WORD_ELLIPSIS262144
(&H40000)
描画領域に収まらない単語を切り捨て、省略記号を追加。
(DT_END_ELLIPSISと同機能?)
DT_NOFULLWIDTHCHARBREAK524288
(&H80000)
DBCS(≒全角文字)が描画領域の右端に達した時は、単語間の半角スペースで改行を行う。
DT_WORDBREAKの同時指定要
DT_HIDEPREFIX1048576
(&H100000)
プレフィックス文字「&(アンパサンド)」の後に続く文字には下線は付かない。
DT_PREFIXONLY2097152
(&H200000)
プレフィックス文字「&(アンパサンド)」の後に続く文字の位置に下線のみを描画する。
図77

定数「DT_TOP, DT_LEFT(どちらも値=0)」は、第2引数で指定したテキストを第4引数で指定した描画領域に上揃え+左揃えで描画します。
定数「DT_CENTER(値=1)」は、テキストを水平方向の中央揃えにします。
定数「DT_RIGHT(値=2)」は、テキストを右揃えにします。
それぞれの値でユーザーフォーム上に描画したのが図78になります。赤い点線の四角が、第4引数で指定した描画領域(実際には描画領域の枠は表示されません)を示します。
なお、文字の背景は透明化せずに白色のままとしています(詳細は「背景色」「背景モード」を参照下さい)。
左揃え、水平中央揃え、右揃え
図78

定数「DT_VCENTER(値=4)」は、テキストを垂直方向の中央揃えにします。
定数「DT_BOTTOM(値=8)」は、テキストを下揃えにします。実際に描画したのが図79です。
なお、この定数は単独指定では機能せず(図79の①③)、定数「DT_SINGLELINE(値=32)」も同時指定(図79の②④)することで、指定の位置に描画されます。
垂直中央揃え、下揃え
図79

定数「DT_WORDBREAK(値=16)」を指定すると、単語間が半角スペースで区切られている場合は、描画範囲内で改行します。但し単語が描画範囲幅よりも長い場合は文字が切られてしまいます(図80の②の3行目)。
また、定数「DT_SINGLELINE(値=32)」を指定すると、図80の③のように「改行せず、1行で描画」します。
スペースでの改行
図80

ちなみに図80の②では、「HやPの後ろに半角スペースが残っている」ようにも見えます。しかし、領域ギリギリになるように文字を調整してみると「最後の文字の後ろには半角スペースが残らず」かつ「改行した次の行の先頭にも半角スペースは無い」状態になるので、実質は「スペースは改行に変化」すると考えて良さそうです。
また「全角スペース」で区切った時は、単語と全角スペースの間(=全角スペースの両サイド)で改行されます。しかし、これはスペースに限らず「全角の文字が入る事による現象(=全角は、どこでも分割されてしまう)」なので、改行後にちょうど全角スペースが来た時には「次行の行頭に全角スペースが残り、左端がズレて見える」ことになります。
一方「vbCrLf」を組み込んだ文字列を指定した場合(図81)には、「DT_TOP+DT_LEFT(値=0)」および「DT_WORDBREAK(値=16)」を指定すると「vbCrLf」の所で改行してくれます。しかし「DT_SINGLELINE(値=32)」では、vbCrLfが記号に置換されるだけで改行自体はしてくれません。
vbCrLfでの改行
図81

テキストにタブ記号(VBAでは「vbTab」)を含めた場合、「DT_TOP+DT_LEFT(値=0)」では図82の左側①のように、「←印」に置き換えられるだけですが、定数「DT_EXPANDTABS(値=64)」を指定すると図82②のように、タブとして機能してくれます。なお、この時のタブの文字数は既定の8文字です。 但しタブは、改行の役割は果たしてくれないので注意が必要です。
vbTabでのタブ挿入
図82

DT_EXPANDTABS(値=64)の注意事項として「DT_WORD_ELLIPSIS、DT_PATH_ELLIPSIS、DT_END_ELLIPSISとの同時指定不可」とありますが、何も表示されない訳でも無く、同時指定した機能も一応役割を果たしているようには見えます(但しDT_PATH_ELLIPSISは機能していない)。しかし正しく表示されない条件が存在するのでしょうから、同時指定しない方が良さそうです。
定数「DT_TABSTOP(値=128)」は、タブの文字サイズを指定するものです。タブの既定は「8文字分」です。
なおこの定数値を指定しただけでは「vbTabは←印に変わる(図83の①)」だけなので、同時に「DT_EXPANDTABS(値=64)」を指定する必要があります。様々な設定値を指定してみたのが、図83になります。
タブ文字数の設定
図83

まず「DT_EXPANDTABS(値=64)+DT_TABSTOP(値=128)」では、既定のタブ文字数(8文字)がタブとして表示(図83の②)されます。
次にタブサイズを設定する位置ですが、Microsoftでは「15ビットから8ビット(下位ワードの上位バイト)に設定」と説明しています。しかし8ビット目は、「定数DT_TABSTOPのフラグを立てるビット目」なので、ビットが重なってしまう事になります。そこで9ビット目より上に1つずつフラグを立ててみました(図83の③~⑥)。
すると確かにタブ文字数は増えていく傾向にあり、12ビット目にフラグを立てる(値=2048 図83の⑥)とフラグを立てない②(既定のフラグ)と同じサイズ?になることが分かりました。
つまり「タブサイズは8~16ビット目で設定」するという事では? と思われます。
なおビットにフラグを立てると「2の〇〇乗」という値となりますが、その〇〇をビット位置と数える手法で「15ビットから8ビット」と言っているのだとすれば、それはそれで合っているとも言えます。
色々な設定を試してみて、図83の⑤の11ビット目にフラグを立てる設定では「一つ下の10ビット目のタブ文字数と変わらない」結果となっています。今回の流れであれば4文字分のはずですが、これはバグなのかもしれません。また13ビットより上は、用途が少ないと考え試していません。
定数「DT_TABSTOP」には「DT_CALCRECT、DT_EXTERNALLEADING、DT_INTERNAL、DT_NOCLIP、DT_NOPREFIXとの同時指定不可」との説明があります。確かに「DT_CALCRECT、DT_EXTERNALLEADING、DT_INTERNAL、DT_NOCLIP」を同時指定するとタブの表示が一部おかしくなります。しかし「DT_NOPREFIX」の同時指定では、&印の後ろの文字には下線が付きますし、&&は&と表示されるので一応正しく機能しているようには見えます。
定数「DT_NOCLIP(値=256)」は、第4引数で指定した描画領域を無視して文字を描画するものです。但し図84のように、文字の書き出しの始点までは無視しないようです。
指定の描画領域を無視
図84

なおDT_NOCLIPを指定すると「TextOut関数」とほぼ同じ機能になるのですが、「処理速度はDT_NOCLIPを使用した方が、TextOut関数使用時より速い」と説明しています。しかし今回試してみたら「TextOutの方が2~3倍速い」ようです。もしかしたら条件により逆転するのかもしれませんが、速さを求めてこのDT_NOCLIP指定をするのは止めた方が良いと思われます。
定数「DT_EXTERNALLEADING(値=512)」は、図85のように行間にフォントの外部レディングを含める設定です。
なお外部レディングとは「ある行のフォント領域上端~直前の行のフォント領域下端の間の幅」という定義のようで、見かけ的には行間隔が広がったようになります。
外部レディング有りの設定
図85

定数「DT_CALCRECT(値=1024)」は、指定した文字の描画に「必要充分な描画領域」を第4引数(RECT構造体)に戻す機能です。文字の描画はしません。
なお戻してくる描画領域は、実行時に指定した描画領域の左上角(RECT.Left、RECT.Top)を基準としています。
ですので、一旦描画したい文字列をDT_CALCRECTでDrawText関数を実行(この時の指定RECT構造体はLeftとTopだけを気にすれば良い)し、実行により必要充分な領域がRECT構造体に入るので、そのRECT構造体を使って通常のDrawText関数を実行すれば、全ての文字がちゃんと描画される(図86の②)事になります(但し、ユーザーフォームからはみ出さない限り)。
必要充分な描画領域を戻す
図86

なおDrawText関数の戻り値には通常「テキストの高さ」が戻るのですが、この時の戻り値と戻される「RECT構造体の高さ値」は同一になります。
定数「DT_NOPREFIX(値=2048)」は、プレフィックス文字(&印)での処理を無効にし、文字「&」として描画します。
まずプレフィックス文字についてですが、DrawTextでは「&印」の後ろの文字に下線が付きます。もし複数個所に付けた場合には「各行の最後の文字のみに下線」が付くことになっているようです。また「&&」とプレフィックス文字を繰り返すと「文字としての&」が出力されます。
なお、プレフィックス文字が&のみなのか、他にも存在するのかは分かりませんでした。
このプレフィックス文字の機能を無くすのが定数DT_NOPREFIXで、この指定により図87の右側②のような出力になります。
プレフィックス文字を無効化
図87

DrawText関数は、描画した文字の高さ(複数行の表示となった時は、全体の高さ)を戻します。ですので1行表示の場合は「指定したフォントの高さ=DrawTextの戻り値」となります。
定数「DT_INTERNAL(値=4096)」は、指定フォントでは無く「システムフォント(=既定フォント)」の高さを基準にして戻り値を戻してきます。
図88の①は既定フォントでの描画、②③は24ピクセルの高さのフォントでの描画ですが、右側③のように「DT_INTERNAL(値=4096)」を指定することで①と同じ高さを取得することが出来ます。
システムフォントでのFont高さを戻す
図88

なおシステムフォントで描画するには、以下の方法があると思います。
 ・SelectObjectでフォントを指定せずに文字描画をする
 ・GetStockObjectでSYSTEM_FONTのハンドルを取得し、SelectObjectで描画道具を選択
定数「DT_EDITCONTROL(値=8192)」は、部分的に表示されている行そのものを表示しない機能です。
描画領域の高さ方向が低い場合、通常は図89の左側①のように「一番下の行は、文字が部分的にしか表示されない」事になります。この「DT_EDITCONTROL(値=8192)」を指定(右側②)すると、部分的な最後の行は非表示になります。
部分的に表示される最後の行を表示しない
図89

描画領域の横幅に文字が収まらない場合、定数「DT_PATH_ELLIPSIS(値=16384)」を指定する事で、途中の単語を省略印「...」にできます。また定数「DT_END_ELLIPSIS(値=32768)」を指定すると、末尾の単語が省略印になります。
なお、文字列を変更するには「DT_MODIFYSTRING(値=65536)」の同時指定が必要との事です。
試した結果が図90です。
文字が1行に収まりきらない時に部分的に省略記号にする
図90

まず「DT_PATH_ELLIPSIS(値=16384)」ですが、対象としている文字列にクセがあり、「¥印」が入るような例えば「"C:¥abc¥def¥efg.jpg"」みたいな文字列に限られるようです。そして、入りきらない「途中の単語=フォルダー名」が省略されます。どうもこの関数を作った人は、定数名の「PATH」からも分かるように「省略するのはフォルダー名」「省略しないのはファイル名」としたかった様です。 ちなみに「半角スペースで区切った文字列」では、途中の単語を省略印にはしてくれません。
結果は、図90の一番左側①のように「フォルダー名が省略(...印)」されます。
次に「DT_END_ELLIPSIS(値=32768)」が対象としている文字列は、半角スペースで区切った文字列でも、PATH名のように¥印で区切った文字列でも有効です。図90の②③のように「省略印(...)が行末に入るように、その前の文字を調整」して、その後ろに「省略印(...)」が続く事になります。
ちなみに、どちらも「DT_MODIFYSTRING(値=65536)」があっても無くても省略印となりますが、Microsoftが同時設定しろと言っているので、同時設定しておいた方が安心です。
定数「DT_RTLREADING(値=131072)」は、右から左に読み書きするヘブライ語、アラビア語対応のものとのことです。日本語の環境で使用しても特に変化は無さそうです。
定数「DT_WORD_ELLIPSIS(値=262144)」は、「DT_END_ELLIPSIS(値=32768)」と同じ機能のようです。値を指定すると、図91のように「末尾が省略印」になります。
文字が1行に収まりきらない時に末尾を省略印にする
図91

ちなみに、この「DT_WORD_ELLIPSIS」には「DT_MODIFYSTRING(値=65536)」の同時指定は不要のようですが、同時指定しても結果は変わりませんでした。
定数「DT_NOFULLWIDTHCHARBREAK(値=524288)」は、DBCS(≒全角文字)が描画領域の右端に達した時は、単語間の半角スペースで改行を行うものです。なお改行で描画するには「DT_WORDBREAK(値=16)」の同時指定が必要です。
結果は図92のようになります。
全角の文字を半角スペースで改行する
図92

日本語のような全角文字の中に「半角スペース」を単語間に入れておくと、図92の中央②のように、半角スペースで全角の単語が改行されます。日本語で、どうしても単語を分割したくない時には便利かもしれません。
ちなみに右側③のように「全角スペースでは改行されない」ので注意が必要です。
定数「DT_HIDEPREFIX(値=1048576)」は、プレフィックス文字(&印)の後ろに続く文字に対し、下線を付けないという処理をします。
また定数「DT_PREFIXONLY(値=2097152)」は、プレフィックス文字(&印)の後ろに続く文字の位置に下線のみを描画し、その他の文字も含めて文字は描画しない処理になります。結果は図93のようになります。
プレフィックス文字の処理をする
図93

4-5-3.文字の色(SetTextColor関数)

SetTextColor関数は、描画する文字の色を指定するものです。API関数宣言文を再掲します。
Declare PtrSafe Function SetTextColor Lib "gdi32" (ByVal hdc As LongPtr, ByVal crColor As Long) As Long
第1引数にはデバイスコンテキスト(DC)のハンドルを指定し、第2引数には文字色をRGB関数などで指定します。
以下は図75を改造した文字色変更のコードで、文字色を「既定色」→「赤色」→「元に戻す」と変更しています。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとします。
  1. '========== ⇩(15) 文字色を指定 ============
  2. Sub TextOut_Draw3()
  3.  Dim str As String      '←表示する文字列
  4.  Dim n As Integer      '←文字数
  5.  Dim Old_Color As Long    '←元の文字色
  6.  str = "Win32APIへようこそ"
  7.  n = LenB(StrConv(str, vbFromUnicode))
  8.  Me.Repaint
  9.  Call TextOut(hdc, 50, 0, str, n)    '←既定の文字色で描画(図95の①)
  10.   Old_Color = SetTextColor(hdc, RGB(255, 0, 0))    '←文字色を赤色に変更
  11.  Call TextOut(hdc, 50, 50, str, n)    '←赤色の文字色で描画(図95の②)
  12.   Call SetTextColor(hdc, Old_Color)    '←文字色を元に戻す
  13.  Call TextOut(hdc, 50, 100, str, n)    '←元の文字色で描画(図95の③)
  14. End Sub
図94

まず291行目「Call TextOut(hdc, 50, 0, str, n)」より前には文字色の設定をしていないので、既定の色での文字描画を行っています(結果は、図95の①)。
293行目「Old_Color = SetTextColor(hdc, RGB(255, 0, 0))」では、文字色を「RGB(255, 0, 0)」の赤色に変更しています。 SetTextColor関数の戻り値は「元の文字色」ですので、既定の文字色が変数Old_Colorに代入されます。
295行目の内容は291行目とほぼ同じ(描画位置が異なる)ですが、293行目で文字色を赤色に変更した後なので、赤色で文字描画を行います(図95の②)。
297行目「Call SetTextColor(hdc, Old_Color)」では再びSetTextColorを呼び出し、文字色を変更しています。ここでは変数Old_Color(293行目で受け取った、元の文字色)を指定していますので、文字色は既定色に戻ります。
299行目の内容は295行目とほぼ同じ(描画位置が異なる)ですが、297行目で文字色を元に戻しているため、既定の色で描画を行います(図95の③)。
描画されるユーザーフォームは、以下のようになります。
文字色を変更して描画
図95

寄り道(CLR_INVALID値)
APIのSetTextColor関数が失敗すると「CLR_INVALIDが戻る」とMicrosoftは説明していますが、このCLR_INVALIDの値がいくつなのかは、よく分かりません。
他サイトを調べてみると「65535」という値が出てきますが、この値は「RGB(255, 255, 0)」で得られる値(=黄色)です。RGB関数の範囲内の値が戻り、それがエラー値というのは変です。その他には「0xFFFFFFFFF」や「0xFFFFFFFF」という値も見掛けます。
上の宣言文でも分かるように「戻り値はLong型なので4バイト」です。ですので先ほどの「Fが9個並ぶ値」はLong型から外れてしまうので、これも変です。
試しにSetTextColor関数を実行する際に、第1引数に「取得を失敗したハンドル値(=0)」を指定してみると、戻り値は「-1」となります。先ほどの「Fが8個」の値は「4バイトの全てに1のフラグが立つ」状態で、Long型はプラスマイナスありますので「0xFFFFFFF = -1」となるはずです。
ですので、SetTextColor関数が失敗した時には「-1(CLR_INVALID)が戻る」と考えて良さそうです。

4-5-4.文字の背景色設定(SetBkColor関数)

SetBkColor関数は、文字の背景色を設定するものです。API宣言文を再掲します。
Declare PtrSafe Function SetBkColor Lib "gdi32" (ByVal hdc As LongPtr, ByVal crColor As Long) As Long
第1引数にはデバイスコンテキスト(DC)のハンドルを指定し、第2引数には文字の背景色をRGB関数などで指定します。
以下は図75を改造した背景色変更のコードで、背景色を「既定色」→「赤色」→「元の色」と変更しています。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとします。
  1. '========== ⇩(16) 文字の背景色の指定 ============
  2. Sub TextOut_Draw4()
  3.  Dim str As String      '←表示する文字列
  4.  Dim n As Integer      '←文字数
  5.  Dim Old_BkColor As Long    '←元の背景色
  6.  str = "Win32APIへようこそ"
  7.  n = LenB(StrConv(str, vbFromUnicode))
  8.  Me.Repaint
  9.  Call TextOut(hdc, 50, 0, str, n)    '←既定の背景色で描画(図97の①)
  10.   Old_BkColor = SetBkColor(hdc, RGB(255, 0, 0))    '←背景色を赤色に変更
  11.  Call TextOut(hdc, 50, 50, str, n)    '←赤色の背景色で描画(図97の②)
  12.   Call SetBkColor(hdc, Old_BkColor)    '←背景色を元に戻す
  13.  Call TextOut(hdc, 50, 100, str, n)    '←元の背景色で描画(図97の③)
  14. End Sub
図96

まず321行目「Call TextOut(hdc, 50, 0, str, n)」より前には背景色の設定をしていないので、既定の背景色での文字描画を行っています(図97の①)。
323行目「Old_BkColor = SetBkColor(hdc, RGB(255, 0, 0))」では、背景色を「RGB(255, 0, 0)」の赤色に変更しています。 SetBkColor関数の戻り値は「元の背景色」ですので、既定の背景色が変数Old_BkColorに代入されます。
325行目の内容は321行目とほぼ同じ(描画位置が異なる)ですが、323行目で背景色を赤色に変更した後なので、赤色の背景で文字描画を行います(図97の②)。
327行目「Call SetBkColor(hdc, Old_BkColor)」では再びSetBkColorを呼び出します。ここでは変数Old_BkColor(323行目で受け取った、元の背景色)を指定していますので、背景色は既定色に戻ります。
329行目の内容は325行目とほぼ同じ(描画位置が異なる)ですが、327行目で背景色を元に戻しているため、既定の背景色で描画を行います(図97の③)。
描画されるユーザーフォームは、以下のようになります。
背景色を変更して描画
図97

寄り道(既定の背景色)
既定の背景色は図97でも分かるように、ここでは「白色」です。試してみると確かにSetBkColor関数で戻される既定背景色の値はRGB(255, 255, 255)でした。 但し、Microsoftでの既定背景色の説明として「コントロールパネルの背景色の設定 (通常は白)」と説明しているように、PC毎に設定変更が可能なようです。
背景色の設定であれば、Windows11の場合「個人用設定」→「背景」→「背景をカスタマイズ」で単色を指定 →「背景色の選択」だと思うのですが、残念ながら色を変更してもGDIの文字の背景色は白色から変化ありませんでした。もちろん再起動をしても同じです。またレジストリ内も探したのですが、それらしい項目は見つかりません。
どこかで変更できる可能性があるならば「背景色の既定は白色」と言い切る事ができませんので、もし白の背景色にしたい場合は「SetBkColor(hdc, RGB(255, 255, 255))」とする必要があると言う事になります。

4-5-5.文字の背景モード設定(SetBkMode関数)

SetBkMode関数は、背景のモードを透明にするか否かの設定です。API宣言文を再掲します。
Declare PtrSafe Function SetBkMode Lib "gdi32" (ByVal hdc As LongPtr, ByVal nBkMode As Long) As Long
第1引数には、デバイスコンテキスト(DC)のハンドルを指定します。第2引数には、以下の背景モード値を指定します。
(定数)内容
TRANSPARENT1下の背景をそのまま残す(透過表現)
OPAQUE2(既定)選択されている背景色で背景を塗りつぶす
図98

「TRANSPARENT(値=1)」は、文字が書かれる下(ここではユーザーフォーム)の背景の上に文字を描画することになります。つまり、文字の背景色は透明となります。
一方で既定の「OPAQUE(値=2)」では、既定の背景色(≒白色)またはSetBkColor関数で設定した背景色の上に文字を描画します。
以下は図75を改造した背景モード変更のコードで、背景モードを「既定モード(選択している背景色で塗りつぶし)」→「背景モードを透明化」→「元の背景モード」と変更しています。
なおデバイスコンテキスト(DC)のハンドルはhDCとして取得済みとします。
  1. '========== ⇩(17) 背景モードの指定 ============
  2. Sub TextOut_Draw5()
  3.  Dim str As String      '←表示する文字列
  4.  Dim n As Integer      '←文字数
  5.  Dim Old_BkMode As Long    '←元の背景モード
  6.  str = "Win32APIへようこそ"
  7.  n = LenB(StrConv(str, vbFromUnicode))
  8.  Me.Repaint
  9.  Call TextOut(hdc, 50, 0, str, n)    '←既定の背モードで描画(図100の①)
  10.   Old_BkMode = SetBkMode(hdc, 1)    '←背景モードを透明化に変更
  11.  Call TextOut(hdc, 50, 50, str, n)    '←透明の背景色で描画(図100の②)
  12.   Call SetBkMode(hdc, Old_BkMode)    '←背景モードを元に戻す
  13.  Call TextOut(hdc, 50, 100, str, n)    '←元の背景モードで描画(図100の③)
  14. End Sub
図99

まず351行目「Call TextOut(hdc, 50, 0, str, n)」より前には背景モードの設定をしていないので、既定の背景モード(塗りつぶし)での文字描画を行っています(図100の①)。
353行目「Old_BkMode = SetBkMode(hdc, 1)」では、背景モードを透明化(値=1)に変更しています。 SetBkMode関数の戻り値は「元の背景モード」ですので、既定の背景モードが変数Old_BkModeに代入されます。
355行目の内容は351行目とほぼ同じ(描画位置が異なる)ですが、353行目で背景モードを透明化に変更した後なので、透明の背景で文字描画を行います(図100の②)。
357行目「Call SetBkMode(hdc, Old_BkMode)」では再びSetBkModeを呼び出します。ここでは変数Old_BkMode(353行目で受け取った、元の既定背景モード)を指定していますので、背景モードは既定の塗りつぶしに戻ります。
359行目の内容は355行目とほぼ同じ(描画位置が異なる)ですが、357行目で背景モードを元に戻しているため、既定の背景モード(塗りつぶし)で描画を行います(図100の③)。
描画されるユーザーフォームは、以下のようになります。
背景モードを変更して描画
図100

Microsoftでは「テキスト、ハッチブラシ、ペンのいずれかの描画を行う前に、現在の背景色で背景を塗りつぶす」と説明しているように、このSetBkMode関数は「文字だけでは無く、直線や円形などのハッチングをした図形」に対しても影響を与えます。
作業工程として、矩形や丸・線を描画する前にも、一旦背景色で塗りつぶし、その上からペンやブラシで描画する という手順なのかもしれません。
まず図101はPie関数で作成した円形ですが、この中で②と④は「CreateHatchBrush(2, RGB(255, 0, 0))」で斜めのハッチングをかけたものです。
円形図形で背景モードを変更して描画
図101

SetBkModeの設定をしない(=既定のOPAQUE(値=2) )②では「ハッチングの下は白色」ですが、SetBkMode関数で透明化(TRANSPARENT(値=1) )をした④は「ハッチングの下はフォームの色」となります。
一方、③のようにブラシで塗りつぶし(既定ブラシ=白色塗りつぶし)をしていると、そのブラシの下は「実は透明」なのかもしれませんが、実際には見えません。
もし③の図形の真ん中をフォームの背景色としたい場合は、CreateBrushIndirect関数で透明ブラシを使用します。ここで「ブラシを透明にしても、背景は白のままでは?」との疑念も起こるのですが、結果は透明になります。どうもCreateBrushIndirect関数で透明化すると、連動してSetBkModeも透明化するようです。
また、線図の背景にもSetBkMode関数は影響を与えます。図102は「MoveToEx + LineTo」で、線種を変更して描画した線です。⑤はSetBkModeの設定をせず、⑥がSetBkMode関数で透明化をした結果です。
良く見ると、破線などの裏側は通常白色の背景となりますが、透明化をするとフォームの背景色となることが分かります。
線図形で背景モードを変更して描画
図102

なお透明化する値は図98のように「1」ですが、試してみると「0」でも「3」でも透明化します。どうも「2以外は全て透明化」するようですが、Microsoftが途中で気が付いて修正してしまうかもしれませんので「透明化には1を指定」した方が安全です。

5.サンプルファイルの仕様

5-1.使い方

一番下の「サンプルファイル」には、上記のサンプルコードを載せてあり、また簡易的に実行されるようにしています。
まず図103のように「Sheet1上のボタンをクリック」することで、UserForm1が起動します。起動と同時に、図38のコード(プロシージャ名=Line_Draw1)が実行され、斜めの直線を描画します。次にユーザーフォーム上のどこかをクリックすると、次のコードである図42(プロシージャ名=Line_Draw2)を実行し折れ線を描画します。
サンプルコードは全部で17個ありますので、クリックの都度描画内容が変わり(内容が同じものもあります)、最後の描画(図99:プロシージャ名=TextOut_Draw5)の次は先頭に戻るというトグル表示にしています。
サンプルコードの実行順番
図103

またユーザーフォーム上には「Pen(ペン)」「Brush(ブラシ)」「Font(フォント)」の3つのチェックボックスがあります。初期状態はOFFとなっていて、各描画は「既定の道具(「よりみち」参照下さい)」での出力になります。
このチェックボックスをONにすると、それぞれの道具設定が描画内容に反映し、図104のように変化します。なお図104の左半分のように、円などの図形に対してはペン種とブラシ種は影響しますが、フォント種は影響しません。逆に文字に対して(図104の右半分)は、フォント種のみが影響します。
なお、チェックボックスの操作では実行する描画コードは移動せず、そのまま道具設定が反映されます。
ペン・ブラシ・フォントを指定しての描画
図104

道具の種類は、以下のように固定としました。コードを変更すれば、異なる道具設定が可能です。
 ・ペン種=太さ3ピクセルの赤色線「CreatePen(0, 3, RGB(255, 0, 0))」
 ・ブラシ種=45°右下がりハッチング「CreateHatchBrush(2, RGB(255, 0, 0))」
 ・フォント種=高さ24ピクセルの既定フォント「CreateFont(24, 0, 0, 0, 0, False, False, False, 128, 0, 0, 0, 0, "")」
終了するには、ユーザーフォームの右上×印をクリックして下さい。
なお今回のプログラムでは「画面拡大率」を考慮していません。図103図104は拡大率100%のものなので、それ以外の拡大率のPCではユーザーフォームの大きさが変わり、描画の位置も違って見えますので御了解下さい。

5-2.プログラム内容

以下はサンプルファイルを動かすためのプログラムですので、GDI描画とはほとんど関係ありません。

5-2-1.ワークシート(Sheet1)

Sheet1上には、フォームコントロールのボタンを1つ配置し、そのボタンにはSheet1モジュールの「UFstartプロシージャ」を登録してあります。
シート上のボタンと登録マクロ
図105

Sheet1モジュールには、下記のように「UFstartプロシージャ」を置き、そこからUserForm1を起動しています。
  1. '========== ⇩(18) ユーザーフォームの起動 ============
  2. Sub UFstart()
  3.  UserForm1.Show
  4. End Sub
図106

5-2-2.ユーザーフォーム(UserForm1)

フォーム上には、3つのCheckBoxと1つのLabelを配置しています。後からサンプルファイルを作っているので、描画される絵と多少ぶつかってしまっています。申し訳ありません。
フォーム上のコントロール類の配置
図107

CheckBoxは、ペン・ブラシ・フォントの道具種のON-OFFの役目をします。Labelは「クリックして下さい」との注意書き用です。なおCheckBoxは3つとも、初期は「チェック無し(Value=False)」状態にしています。
以下はフォームモジュールのコードです。
先頭の宣言部ではWin32API関数宣言を行っていますが、図02などで紹介した宣言文を全て宣言しています。同じ宣言文(先頭にPrivateは付けています)ですので、ここでのコード紹介は省略します。なおサンプルファイルでは使われないAPI宣言も存在することを御了承下さい。
また32ビット版Excelのために「#If Win64 Then ~ #Else ~ #End If」で実行するAPI宣言を分けています(32ビットでは、64ビットの宣言から「PtrSafe」を削除しています)。
その後で、API宣言内でも使用している「POINTAPI」「RECT」「LOGPEN」「LOGBRUSH」「LOGFONT」の構造体の定義を行っています。この構造体についても重複しますので省略します。
その後の部分で、今回使用する共通変数の宣言等を図108のように行っています。
  1. '========== ⇩(19) 共通変数の宣言等 ============
  2. Private hwnd As LongPtr   'UserFormウィンドウハンドル
  3. Private hdc As LongPtr   'UserFormウィンドウのデバイスコンテキストハンドル
  4. Private hPen As LongPtr    '設定ペン種のハンドル
  5. Private hBrush As LongPtr   '設定ブラシ種のハンドル
  6. Private hFont As LongPtr   '設定フォント種のハンドル
  7. Private cnt As Integer   '現在表示しているプロシージャの順番
  8. Const Maxcnt = 17      'プロシージャの個数
図108

381~382行目は、ウィンドウのハンドルとデバイスコンテキスト(DC)のハンドル変数宣言です。
384~386行目は、既定では無く新たに設定するペン・ブラシ・フォントのハンドルを格納する変数です。
388行目の変数cntは、現在表示しているプロシージャの順番を表し、値はユーザーフォームをクリックした時に発生するClickイベント(図111)内で変更されます。なおcnt値が使用されるのは、実行プロシージャを選択する時(図112)です。
389行目「Const Maxcnt = 17」はプロシージャの最大数の設定で、cnt値の折り返しに使用しています。
フォームが起動される時に呼び出されるInitializeイベントプロシージャが下記になります。
  1. '========== ⇩(20) フォーム起動時の設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Width = 225 + (Me.Width - Me.InsideWidth)
  4.  Me.Height = 150 + (Me.Height - Me.InsideHeight)
  5.  hwnd = FindWindow("ThunderDFrame", Me.Caption)   'ウィンドウハンドル取得
  6.  hdc = GetDC(hwnd)     'デバイスコンテキスト(DC)のハンドル取得
  7.  hPen = CreatePen(0, 3, RGB(255, 0, 0))
  8.  hBrush = CreateHatchBrush(2, RGB(255, 0, 0))
  9.  hFont = CreateFont(24, 0, 0, 0, 0, False, False, False, 128, 0, 0, 0, 0, "")
  10. End Sub
図109

402~403行目では、フォームのサイズを決めています。大きさに特に意味はありません。
405行目「hwnd = FindWindow("ThunderDFrame", Me.Caption)」では、フォームのウィンドウハンドルを取得します。
FindWindow関数の第1引数にはユーザーフォームを意味する「"ThunderDFrame"」を、第2引数にはウィンドウ名を「Me.Caption」として指定しています。
406行目「hdc = GetDC(hwnd)」では、ユーザーフォーム上のデバイスコンテキスト(DC)ハンドルを取得しています。
GetDC関数を使用しているため、「クライアント領域(図05参照)」のDCハンドルとなります。引数には405行目で取得したウィンドウハンドルを指定します。
408~410行目では、新たなペン・ブラシ・フォントを設定しています。
408行目「hPen = CreatePen(0, 3, RGB(255, 0, 0))」では、「実線(値=0)」「太さ3ピクセル(値=3)」「赤色(値=RGB(255, 0, 0))」のペン種としています。
409行目「hBrush = CreateHatchBrush(2, RGB(255, 0, 0))」では、「45度右下がりのハッチ(値=2)」「赤色(値=RGB(255, 0, 0))」のブラシ種としています。
410行目「hFont = CreateFont(24, 0, 0, 0, 0, False, False, False, 128, 0, 0, 0, 0, "")」は、「高さ24ピクセル(値=24)」「日本語のシフトJIS(値=128)」「既定フォント種(最後の引数値="")」のフォント種としています。
それぞれのハンドル値は、変数hPen、hBrush、hFontに代入しています(変数はモジュール内共通変数)。
フォームが表示される時に発生するActivateイベントのプロシージャが以下になります。
実行内容は422行目「Call UserForm_Click」のみで、フォームの起動と同時に図111が呼び出され、1番目のサンプルコードが実行されて単線が描画されます。
  1. '========== ⇩(21) フォーム表示時の設定 ============
  2. Private Sub UserForm_Activate()
  3.  Call UserForm_Click
  4. End Sub
図110

フォーム上をクリックした時には以下のClickイベントプロシージャが実行されます。
なおフォーム上と思ってクリックした場所にCheckBoxコントロールがあった場合には、CheckBoxのClickイベントが発生してしまい「UserForm_Clickイベントは発生しない」ので注意が必要です(Labelコントロールをクリックした場合には、今回イベント処理をしていない為、何も起こりません)。
  1. '========== ⇩(22) フォームクリック時のイベント ============
  2. Private Sub UserForm_Click()
  3.  cnt = cnt + 1
  4.  If cnt > Maxcnt Then cnt = 1
  5.  Call Exec
  6. End Sub
図111

432行目「cnt = cnt + 1」では、モジュール変数cnt(現在表示されているプロシージャの順番)を次の順番に変更します。
しかし「現在の表示が一番最後(=Maxcnt)」だった場合は、存在しないプロシージャを指す事になってしまいますので、433行目「If cnt > Maxcnt Then cnt = 1」でMaxcntを越えているか否かを調べ、越えていたら「1(=先頭のプロシージャ)」にします。
新たに表示するプロシージャの順番が決まったら、435行目「Call Exec」で図112に移り、描画を実行するプロシージャを呼び出します。
描画を担当する各プロシージャを呼び出すのが以下です。このプロシージャは「フォームをクリックした時」に呼び出されるのとは別に、図113以下のCheckBox_Clickイベントプロシージャからも呼び出されます。
なお図112の内容を、図111のUserForm_Click内で実行させても良いのですが、チェックボックスをON-OFFした時に「描画内容を次に送らずに再描画」しようとするとUserForm_Click内が複雑になりそうだったため、プロシージャを分けています。
またプロシージャ名を配列にして、もっと短いコードで実行できるようにもしたかったのですが、フォームのプロシージャ呼び出しがRunメソッドでは出来ず、このような形になってしまいました。
  1. '========== ⇩(23) 描画の実行(各プロシージャを実行) ============
  2. Private Sub Exec()
  3.  Select Case cnt
  4.   Case 1:   Call Line_Draw1
  5.   Case 2:   Call Line_Draw2
  6.   Case 3:   Call Ellipse_Draw1
  7.   Case 4:   Call Chord_Draw1
  8.   Case 5:   Call Pie_Draw1
  9.   Case 6:   Call Rectangle_Draw1
  10.   Case 7:   Call Polygon_Draw1
  11.   Case 8:   Call Polygon_Draw2
  12.   Case 9:   Call SetPixel_Draw1
  13.   Case 10:   Call SetPixel_Draw2
  14.   Case 11:   Call SetPixel_Draw3
  15.   Case 12:   Call SetPixel_Draw4
  16.   Case 13:   Call TextOut_Draw1
  17.   Case 14:   Call TextOut_Draw2
  18.   Case 15:   Call TextOut_Draw3
  19.   Case 16:   Call TextOut_Draw4
  20.   Case 17:   Call TextOut_Draw5
  21.  End Select
  22. End Sub
図112

452行目「Select Case cnt」は、モジュール内変数であるcnt(表示すべきプロシージャの順番)を見て、制御を割り振っています。
例えば「cnt = 2」ならば、454行目に飛び「Call Line_Draw2」を実行するという具合です。変数cntは図111で最大Maxcntに制御されていますので、453~469行目のどれかが実行されることになります。
フォーム上のCheckBox1(ペン種のON-OFF用)を操作した時には以下のClickイベントプロシージャが呼び出されます。
  1. '========== ⇩(24) ペン種チェックボックスのON-OFF ============
  2. Private Sub CheckBox1_Click()
  3.  Static Previous_Pen As LongPtr    '←既定ペンのハンドル
  4.  If Me.CheckBox1.Value = True Then    '←チェック有の時
  5.   Previous_Pen = SelectObject(hdc, hPen)    '←設定ペンを選択する
  6.  Else
  7.   Call SelectObject(hdc, Previous_Pen)    '←ペンを元に戻す
  8.  End If
  9.  Call Exec    '←現cntのプロシージャで再描画
  10. End Sub
図113

チェックボックスは、チェックが付いているとValue値がTrue、付いていないとFalseとなります。
484行目「If Me.CheckBox1.Value = True Then」でチェックの有無を調べ、チェックが付いている場合は485行目「Previous_Pen = SelectObject(hdc, hPen)」を実行します。
右辺のSelectObject関数の第1引数にはデバイスコンテキスト(DC)のハンドルを指定し、第2引数には図109の408行目で作成したペン種のハンドルを指定します。そしてSelectObject関数からの戻り値は左辺の変数Previous_Penに代入します。
この変数Previous_Penは、482行目で「Staticステートメント」で宣言されているため、フォームが表示されている間は記憶されていることになります。
チェックが外れた時には487行目「Call SelectObject(hdc, Previous_Pen)」が実行されます。第1引数にはDCハンドルを、第2引数には485行目で得た「既定のペン種のハンドル(変数Previous_Pen)」を指定しますので、ペン種は既定状態に戻る事になります。
なお、フォーム上のCheckBoxは初期にOFF状態としていますので、最初にCheckBox1_Clickが呼び出された時には「チェックボックスにチェックが入った(Value=True)」事になります。ですので必ず「487行目よりも485行目の方が、先に実行」されるので、変数Previous_Penが初期値ゼロのまま487行目が実行される事はありません。
ペン種の設定が完了したら、490行目「Call Exec」で図112を呼び出し、描画を行います。この時変数cntの値は変更させていませんので「同じ描画内容で、ペン種が変更」され、ペン種別の描画内容の違いが分かり易くなります。
フォーム上のCheckBox2(ブラシ種のON-OFF用)を操作した時には以下のClickイベントプロシージャが呼び出されます。
  1. '========== ⇩(25) ブラシ種チェックボックスのON-OFF ============
  2. Private Sub CheckBox2_Click()
  3.  Static Previous_Brush As LongPtr    '←既定のブラシのハンドル
  4.  If Me.CheckBox2.Value = True Then
  5.   Previous_Brush = SelectObject(hdc, hBrush)
  6.  Else
  7.   Call SelectObject(hdc, Previous_Brush)
  8.  End If
  9.  Call Exec
  10. End Sub
図114

内容は、ペン種の図113と同じです。
505行目「Previous_Brush = SelectObject(hdc, hBrush)」で、図109の409行目で作成したブラシ種のハンドルを指定します。 507行目「Call SelectObject(hdc, Previous_Brush)」で、ブラシ種を既定に戻しています。
フォーム上のCheckBox3(フォント種のON-OFF用)を操作した時には以下のClickイベントプロシージャが呼び出されます。
  1. '========== ⇩(26) フォント種チェックボックスのON-OFF ============
  2. Private Sub CheckBox3_Click()
  3.  Static Previous_Font As LongPtr    '←既定のフォントのハンドル
  4.  If Me.CheckBox3.Value = True Then
  5.   Previous_Font = SelectObject(hdc, hFont)
  6.  Else
  7.   Call SelectObject(hdc, Previous_Font)
  8.  End If
  9.  Call Exec
  10. End Sub
図115

内容は図113図114と同じです。
525行目「Previous_Font = SelectObject(hdc, hFont)」で、図109の410行目で作成したフォント種のハンドルを指定します。527行目「Call SelectObject(hdc, Previous_Font)」では、フォント種を既定に戻しています。
サンプルファイルでは図115図116の間に、「描画作業」の説明文内で使った「実際に描画するプロシージャ」を置いています。
フォームを終了する際は、以下のQueryCloseイベントプロシージャが実行されます。
  1. '========== ⇩(27) フォームの終了処理 ============
  2. Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
  3.  Call DeleteObject(hPen)
  4.  Call DeleteObject(hBrush)
  5.  Call DeleteObject(hFont)
  6.  Call ReleaseDC(hwnd, hdc)
  7. End Sub
図116

542行目「Call DeleteObject(hPen)」では、図109の408行目で作成したペン種を削除しています。543~544行目も同様にブラシ種・フォント種を削除しています。
546行目「Call ReleaseDC(hwnd, hdc)」は、図109の406行目で取得したデバイスコンテキスト(DC)を解放しています。

アプリ実例・関連する項目

Excel図形等の位置、ディスプレイ上の位置の取得

サンプルファイル

フォーム上に図形や文字をGDI描画(its-047.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。