Home / VB.NET

VB.NET

Windows 界では何年も前から「やれ .Netだ、マネージドコードだ」と盛んなようだが、必要に迫られちょっとだけ VB.NET を弄ってみた。
お粗末な言語仕様の奥には .NET が鎮座するようで、マネージドコードたる物、面倒なだけにしか思えない…

未使用のメソッドパラメータ(関数引数)

警告されない。
C や C++ のように引数の型だけ書いておいて引数名を省略する、なんて事が出来ない。
未使用でも警告されないので、不要なパラメータと判っている場面でも大抵は放置されている。
言語失格 > VB

非短絡評価(非ショートサーキット)

実は正しい呼び方を知らない。複数条件書いた場合、条件を満たそうが満たすまいが、全部評価しちゃうという規則。
VB界の常識と言うか伝統なんだろうけど、確か、.NET 出始めの頃、MSDN に短絡評価するようになった、と書かれてあったのを思い出した。

知らない人にはバグの素であり、本当に評価しているのか見てみる。
len の長さと str が Nothing でないか検査するコード例:

        If len <= 0 Or str Is Nothing Then
00000029  cmp         ebx,0 
0000002c  setle       al   
0000002f  movzx       eax,al 
00000032  cmp         dword ptr [esi],0 
00000035  sete        dl   
00000038  movzx       edx,dl 
0000003b  or          eax,edx 
0000003d  je          00000052 
            ...
        End If
00000052  nop

はぁ~、短絡じゃない。過去のソースが動かなくなるせいか変わってない。当時の MSDN は何だったんだ?
最適化オプションででも対応してくれて良い気がするが…

と思っていたら、AndAlso、OrElse なるキーワードが有るそうで、こちらが望み通りになる。
しかしまぁ何とも不恰好な構文。

        If len <= 0 OrElse str Is Nothing Then
00000010  cmp         edi,0 
00000013  jle         00000019 
00000015  test        ebx,ebx 
00000017  jne         00000020 
            Return 1
00000019  mov         esi,1 
0000001e  jmp         00000025 
        End If
00000020  nop              

一方、Select Case にて OR 条件(カンマ使用)を書いてみると、こちらは条件を満たした処で止め、以降の余計な評価はしない。
場合に拠っては if の代用として使えるかも…

            Case &H81 To &H9F, &HE0 To &HFC       ' == _ismbblead
000000e1  nop              
000000e2  cmp         dword ptr [ebp-18h],81h 
000000e9  jl          000000F6 
000000eb  cmp         dword ptr [ebp-18h],9Fh 
000000f2  jg          000000F6 
000000f4  jmp         00000108 
000000f6  cmp         dword ptr [ebp-18h],0E0h 
000000fd  jl          0000011C 
000000ff  cmp         dword ptr [ebp-18h],0FCh 
00000106  jg          0000011C 
...
        End Select
0000011c  nop

Not 演算子

これまた基本中の基本だが、C/C++言語のノリで書いていると痛い目に逢う。
VB(.NET)では Integer のような整数型変数を If Not xxx Then のように評価しては駄目。

例えば hoge が0かどうか判定しようと次のように書くと、
not 命令で EAX = FFFFFFFE となり、test 命令で0にならないので真にはならない。

    Protected hoge As Integer = 1

    Private Sub xxx(...
        ...
        If Not hoge Then
0000000d  mov         eax,dword ptr [esi+000000E8h] 
00000013  not         eax  
00000015  test        eax,eax 
00000017  je          00000029 
            MsgBox("偽!")
          ...
        End If
00000029  nop              
    End Sub

コードを踏襲するなら次のように書くべきだろう。

        If Not (hoge <> 0) Then
0000000d  cmp         dword ptr [esi+000000E8h],0 
00000014  jne         00000026 
            MsgBox("偽!")
          ...
        End If
00000026  nop              

尤も、MSDN には
2 つのブール式の論理否定を求めます。また、2 つの数式のビットごとの否定を求めます。
とあり、ブール型 (Boolean) または数式を指定しなきゃならない。

VBのルールと言えばそれまでだが、C#とバックエンドを共通化したのだから、せめて警告を出すべきだ。

座標変換

良く間違えるのでメモ。

PointToScreen や RectangleToScreen はクライアント座標をスクリーン座標へ変換すると言う事。
例えばフォームに於いて (100,100) の位置に確実に子ウィンドウが存在する状況で、そのスクリーン座標を得るには↓のようにする。

Dim c As System.Windows.Forms.Control = _
    GetChildAtPoint(New System.Drawing.Point(100, 100))

Dim pt As System.Drawing.Point = Me.PointToScreen(c.Location)
Dim rc As System.Drawing.Rectangle = _
    Me.RectangleToScreen(New System.Drawing.Rectangle(c.Location, c.Size))

Debug.WriteLine("PointToScreen " & pt.ToString)
Debug.WriteLine("RectangleToScreen " & rc.ToString)

もう1つ、矩形と言えば (x1,y1)-(x2,y2) をイメージするが、VB の Rectangle は右下隅でなく、幅, 高さを保持する事。ToString もそう表示する。
Win32 API で御馴染みの RECT とは異なる


フォーム内のコントロールが子を持つならば、そのコントロールの RectangleToScreen を呼ぶ。
例えば GroupBox 内のラベルコントール。

Dim rc As System.Drawing.Rectangle = _
    Me.GroupBox1.RectangleToScreen( _
        New System.Drawing.Rectangle(lbl.Location, lbl.Size))

Debug.WriteLine("RectangleToScreen " & rc.ToString)

ラベル lbl を GroupBox1.lbl のように書かなくても構わない処が紛らわしい。と言うか余計なお節介だ

フォームの直接の子の座標へ変換する手も有り。

Dim pt As System.Drawing.Point = lbl.Location
pt.Offset(Me.GroupBox1.Location.X, _
          Me.GroupBox1.Location.Y)

Dim rc As System.Drawing.Rectangle = _
    Me.RectangleToScreen( _
        New System.Drawing.Rectangle(pt, lbl.Size))

ByRef とポインター

C ではポインター、アドレス演算子(&)、間接演算子(*)が使え、C++ なら参照も使える。

int c = 5;
int *p = &c;
printf("%d\n", *p);

VB ではポインターが無いのでアドレスを関数へ渡すには ByRef と書く。所謂、参照。
VB 界に骨を埋めるつもりなら無問題だが、現実には WinAPI や C の DLL を呼ぶ場面も少なくない。
そこで問題となるのが参照では NULL ポインタを渡せないって事(多分)。

そんなの言語仕様で幾らでも対応出来るように思うのだが…
仕方ないので ByVal で数値0を渡さねばならぬ。つまり宣言を ByVal p As IntPtr とするもの。
簡単だが実に○鹿馬○しい。何の為のコンパイラか?

そんな宣言の関数呼び出しに0と書くのもアレなので相応しい定数を探すと IntPtr.Zero プロパティが良さげ。MSDN でも見かけたのでこれがデフォだろう。

func(IntPtr.Zero)
又は
Dim nul As IntPtr
func(nul)

ByRef の呼び出し

VB 界のみ
次のような ByRef 宣言の関数に即値を与えて呼び出して問題無いのだろうか?

Private Function func(ByRef n As Integer) As Integer
    n += 2
    Return n
End Function

呼び出しコードを見ると、スタックに自動変数を宣言したかの如く生成されるので問題は無さそうだ。

00000004  xor         eax,eax 
00000006  mov         dword ptr [ebp-4],eax 
00000009  nop              
        func(9)
0000000a  mov         dword ptr [ebp-4],9 
00000011  lea         ecx,[ebp-4] 
00000014  call        dword ptr ds:[00A55324h] 

尤も、適切に ByVal 宣言しておくのが無駄が無い。

        func(9)
00000004  mov         ecx,9 
00000009  call        dword ptr ds:[00A55324h] 

文字列のマーシャリング

文字列に対する既定のマーシャリング - 固定長の文字列バッファ より:
StringBuilder バッファを初期化してその容量を N にする際、マーシャラはバッファのサイズを (N+1) 文字に設定する。+1 は...

--> 勿論これは C文字列の事を指している。
VB は C終端文字なんぞ気にしなくて良い事を示唆する一方、Win32 GetWindowText の例はそうでも無い事を示してある。

Public Function GetText() As String
    Dim sb As New StringBuilder(256)
    Win32API.GetWindowText(h, sb, sb.Capacity + 1)
    Return sb.ToString()
End Function

StringBuilder(256) と書けば最大 256文字格納出来る訳だけど、 GetWindowText は格納領域サイズを引数に要求している(終端文字を含めて)。 だから 256 + 1 = 257 を渡すべし! と言う例。 汎用的に sb.Capacity + 1 と書いてある処は、面倒くさがりなのか説明無し。

但し、StringBuilder の Capacity は変動するんじゃないかと思う。
何故なら、例えば引数無しで StringBuilder を生成すると Capacity は16である。
しかし sb.Append によって16文字以上代入出来ない訳じゃない。
手元でやってみると Capacity は 32 へ拡大されたので、必要に応じて倍々に増加させている節がある。
この辺はバージョンによって動きが異なるいい加減な作りなよう。

と言う按配で、Capacity が縮小されない限り sb.Capacity + 1 を渡せば問題は起きないはずだが初期化時と異なる値が渡る時も有る。

さて、Capacity ==? StringBuilder のバッファサイズを超えて書き込まれると どうなるか?
デバッガでは次のようなメッセージを食らう。

'System.IndexOutOfRangeException' のハンドルされていない例外が xxx.exe で発生しました。
追加情報 : 警告 : StringBuilder のバッファがアンマネージ コードにより オーバーフローしました。 処理が不安定になる可能性があります。 集成を行う前に、StringBuilder に十分な領域を割り当てましたか?

この辺もバージョンによって動きが異なるようで、例外キャッチしないのも有った。

さらに、アンマネージ側が MBCS だと StringBuilder.Capacity の倍までは例外にならない例も有り、相当にいい加減な仕様/作りみたいだ。

否、「ちゃんとした仕様だ!」と言われ兼ねないが、仕様がどんどん変わっているらしき処が「い い 加 減」。
これを MS流 で進歩と呼ぶらしいが…

デバッガで String を追う

VBのウォッチ等は VC のそれと比べて貧弱。
ebp などのレジスタは扱えないし、変数のアドレスも表示してくれない。
必要性が低いのかも知れないが、手を抜く事も有るまいに…

String を自動変数に定義するとこんなコードになる。

        Dim s As String = "ぶいびー"
00000016  mov         eax,dword ptr ds:[061C156Ch] 
0000001c  mov         dword ptr [ebp-10h],eax

この時の EAX = 051DF66C
そのアドレスを(メモリで)覗いてみると String の実体らしき内容が見える。

0x051DF66C  f8 9a bf 79 05 00 00 00  o鬨€
0x051DF674  04 00 00 00 76 30 44 30  ....v0D0
0x051DF67C  73 30 fc 30 00 00 00 00  s0u0....
0x051DF684  00 00 00 80 
                        f8 9a bf 79  ...€o鬨€
0x051DF68C  69 00 00 00 68 00 00 00  i...h...
0x051DF694  4d 00 69 00 63 00 72 00  M.i.c.r.
0x051DF69C  6f 00 73 00 6f 00 66 00  o.s.o.f.

先頭 4 byte はお決まりっぽい。もしくはアドレスか…
その後の 4 byte は文字数+1 のようだが?
その後の 4 byte は文字数らしい。
その後にユニコード文字列が並んでいる。

"ぶいびー" のユニコード値は

76 30 44 30 73 30 FC 30

その後の 8 byte が String の内容なのか不明なので、適当に文字列を追加して新しいインスタンスへ入れ替えてみる…

        s += "a"
00000019  mov         edx,dword ptr ds:[061C1570h] 
0000001f  mov         ecx,esi 
00000021  call        788F0DF0 
00000026  mov         edi,eax 
00000028  mov         esi,edi 

0x051E22D8  f8 9a bf 79 06 00 00 00  o鬨€
0x051E22E0  05 00 00 00 76 30 44 30  ....v0D0
0x051E22E8  73 30 fc 30 61 00 00 00  s0u0a...
0x051E22F0  00 00 00 00 00 00 00 00  ........

0 の羅列に変わったのでユニコード文字列の後には何も無さそう。否、00 が付くとも見かけた気がするが…

書籍に拠ると次の構造らしい。
同期インデックス ?不明
容量 文字数+1 じゃない場合も有る?
文字数
16bit Unicode文字…

後半は WTypes.h に有る次の構造体のよう。

typedef struct tagBSTRBLOB
{
    ULONG cbSize;       /* ... 文字数 */
    /* [size_is] */ BYTE *pData;
} BSTRBLOB;

BSTR の構造を知らないが String はそれらしく、 cbSize の b は文字数であってバイト数じゃない処が紛らわしい。

String をアンマネージへ(参照渡し)

MSDN に図解説明が載っており、それを参考に試す。

まず、C で作成した DLL へ String を渡してみる。
次のように Unicode で ByRef とする。

Declare Unicode Function xxx Lib "xxx.dll" (ByRef s As String) As Integer

VB 呼び出しでアドレス(レジスタ)を見ておく。

str-ref
        Dim s As String = "ぶいびー"
00000010  mov         eax,dword ptr ds:[061C12B4h] 
00000016  mov         dword ptr [ebp-8],eax 
        Dim r As Integer = xxx(s)
00000019  lea         ecx,[ebp-8] 
0000001c  call        dword ptr ds:[00A55328h] 

EAX = 051CA558 ... String s の実体
EBX = 00000000 
ECX = 0013F68C ... 051CA558 を指すポインタ

0x051CA558  f8 9a bf 79
0x051CA55C  05 00 00 00  ....
0x051CA560  04 00 00 00  ....
0x051CA564  76 30 44 30  v0D0
0x051CA568  73 30 fc 30  s0u0
0x051CA56C  00 00 00 00
0x051CA570  00 00 00 80

0x0013F68C  58 a5 1c 05

DLL 関数で見ると 0013F68C ではなく、少しずれた 0013f61c が渡されていた。
- s,32  0x0013f61c "d&"	char *
  [0x0] 0x64
  [0x1] 0x26
  [0x2] 0x18
  [0x3] 0x00
参照型なのでその先を見ると確かに "ぶいびー" だ。
0x00182664  76 30 44 30  v0D0
0x00182668  73 30 fc 30  s0u0
0x0018266C  00 00 ad ba

Declare で Unicode 指定しなければ日本語版ではマーシャラーが Shift-JIS に変換する。
- s,32	0x0013f60c "・"	char *
  [0x0]	0x8c
  [0x1]	0x08
  [0x2]	0x18
  [0x3]	0x00
手前の文字数が 8 を示すのは偶然か、または bytes を示しているのか?…
0x00180880  f4 f5 13 00  oo..
0x00180884  0d f0 ad ba  .d-o
0x00180888  08 00 00 00  ....
0x0018088C  82 d4 82 a2  ?O?¢
0x00180890  82 d1 81 5b  ?N?[
0x00180894  00 22 00 00  ."..

最後の文字 81 5b を 30 31 (="01")へ書き換え、VB側が変更されるか?
0x00180888  08 00 00 00  ....
0x0018088C  82 d4 82 a2  ?O?¢
0x00180890  82 d1 30 31  ?N01
0x00180894  00 22 00 00  ."..
VB 側へ制御が戻ると新しい String インスタンスへ置き換わっている。
	s	"ぶいび01"	String

0x051CD28C  f8 9a bf 79 06 00 00 00  o??y....
0x051CD294  05 00 00 00 76 30 44 30  ....v0D0
0x051CD29C  73 30 30 00 31 00 00 00  s00.1...

図は MSDN の物にマーシャラーを書き加えたものだけど、2 次バッファ云々の処が判らない。2 次バッファにコピーする時に文字セットを変換し、その参照を呼び出し先に渡す、とあるので、アンマネージ ヒープ内の事だろうと思うが…
どの道、String の参照渡しは扱い難いので使う事は無いだろう。

String をアンマネージへ(値渡し)

次は値渡し。

Declare Unicode Function xxx Lib "xxx.dll" (ByVal s As String) As Integer

一見、何も問題無いように見えるが、実は関数呼び出し後に String オブジェクトは新たに生成され、その変数 s(ポインタ)は新たな String オブジェクトを指すようになる。
げげぇ!

str-val
        Dim s As String = "ぶいびー"
00000013  mov         eax,dword ptr ds:[061D12B4h] 
00000019  mov         dword ptr [ebp-8],eax 
        Dim r As Integer = xxx(s)
0000001c  lea         ecx,[ebp-8] 
0000001f  call        dword ptr ds:[00A65328h] 
00000025  mov         esi,eax 

変数 s を見る為に ebp を見る。
EBP = 0012F694 

call の直前:
0x0012F68C  58 a5 1d 05  String オブジェクト
0x0012F690  7b 52 a6 00  
0x0012F694  9c f6 12 00  ebp

0x051DA558  f8 9a bf 79  o??y
0x051DA55C  05 00 00 00  ....
0x051DA560  04 00 00 00  ....
0x051DA564  76 30 44 30  v0D0
0x051DA568  73 30 fc 30  s0u0
0x051DA56C  00 00 00 00  ....

call の直後:
0x0012F68C  8c d2 1d 05  新しい String オブジェクト
0x0012F690  7b 52 a6 00  {R|.
0x0012F694  9c f6 12 00  ebp

0x051DD28C  f8 9a bf 79  o??y
0x051DD290  05 00 00 00  ....
0x051DD294  04 00 00 00  ....
0x051DD298  76 30 44 30  v0D0
0x051DD29C  73 30 fc 30  s0u0
0x051DD2A0  00 00 00 00  ....

驚くのはこれからで、Declare の Unicode を外し、マルチバイトで渡すと call の後の String は悲惨になる。

0x051CD28C  f8 9a bf 79  o??y
0x051CD290  05 00 00 00  ....
0x051CD294  04 00 00 00  ....
0x051CD298  76 30 44 30  v0D0
0x051CD29C  00 00 00 00  ....   ←消えた!

何となく4文字ではなく4bytesで切り取られた様子。
最新版はどうなってるか知らないが、知ってか知らずか出荷している時点で「駄目だ、こりゃ」。

尤も、巷の噂ではマーシャル属性だかを指定すれば回避されるらしいが…
VB で書き戻しを防ぐ方法が無さそうで、かなり「イタイ」作りに思う。

なお、書き戻しと言っても渡した String オブジェクトが破棄される訳では無さそうなので、渡す前にもう1つ、例えば
Dim s2 As String = s
のように元の String オブジェクトを見失わないよう参照を記憶しておくか、あるいは
Dim r As Integer = xxx((s))
のように渡す変数(参照)を括弧で囲んで(複製して)呼ぶのが簡単かも知れない。

LeftB 関数もどき

Let It Be じゃなく昔の LeftB 関数。 探せばネットにゴロゴロしているが、少々違った用途に使いたいので改造。
VB だと何故か goto 使う方がすっきり書ける。:-)

' str : [in/out] 指定文字列
' len : [in]     取得バイト数(先頭から)、あるいは最大バイト数。
'
' 戻り値 : 文字列 str をクリップしたか否かを返す。
'
' 文字列(ユニコード)を Shift-JIS バイト配列へ変換して指定バイト数を得る。
' バイト配列長が len より短かい場合は何もしないが、str が Nothing であったな
' ら String.Empty を代入する。
'
' この関数の目的は Left 関数と言うよりは、Shift-JIS バイト列へ変換した際に最
' 大バイト数を超えないようにクリップする事を想定している。
' 尤も、ユニコード文字数のバイト換算は高々2倍なので、その比較結果によっては
' バイト列変換は省かれる。
'
Public Function xxxxxx(ByRef str As String, ByVal len As Integer) As Integer
    Dim clip As Integer = 0

    If str Is Nothing Then GoTo EmptyReturn

    If len <= 0 Then
        clip = str.Length               ' 文字数の有無がクリップ。
        GoTo EmptyReturn
    End If

    If str.Length() <= (len >> 1) Then  ' クリップは起こらない。
        Return 0
    End If

    ' Shift-JIS バイト列
    Dim arr As Byte() = System.Text.Encoding.GetEncoding(932).GetBytes(str)

    If arr.Length() <= len Then         ' クリップ不要。
        Return 0
    End If

    clip = 1

    ' 先行バイトで終わりそうなら切り詰める。
    ' もし、先行バイト1文字を要求されたら空文字になる。
    Select Case arr(len - 1)
        Case &H81 To &H9F, &HE0 To &HFC       ' == _ismbblead
            If len <= 1 Then GoTo EmptyReturn
            len -= 1
    End Select

    ' バイト列から指定バイトだけユニコードに戻して終わる。
    str = System.Text.Encoding.GetEncoding(932).GetString(arr, 0, len)
    Return clip

EmptyReturn:  ' 空文字を返して終わるケース。
    str = String.Empty
    Return clip
End Function

Shift-JIS 先行バイト判定は VC の _ismbblead 実装を模倣したかったのだけれど、テーブルを動的に作っているのか、不明な為、マニュアルと試した結果で範囲を決めた。また、先頭文字から順になめて判断しなくて良いのか不安だが、壊れた文字が VB ユニコード文字列に入り込んでいる状況は想定してないので悪しからず。

ListBox:タブ揃え

ListBox は Windows 創世記に作られただけあって中々質実剛健。
いやパクられたと言うべきか…
何も知らずに見た目だけ優先してリストビューなんかへ替えちゃったりするとユーザーから怒りが届いたりする。

いやぁ、見栄えも重要ですから、と言う人の為に
問題になりがちなのが複数項目を持つテキストでの桁揃え。
古くは項目間を適当な空白で埋める小細工で良かったのだけれど、プロポーショナルフォントが当り前な今では通用しない。

となればエディタでお馴染み「タブ揃え」。
意地悪な VB にはメソッドが無さそう。然るべきメッセージを送ればいいだけ…

Public Class xxx

    Public Shared Sub Tabs(ByRef lst As System.Windows.Forms.ListBox, _
                           ByVal ParamArray pos() As Integer)
        Dim count As Integer = UBound(pos)

        For i As Integer = 0 To count
            pos(i) *= 4
        Next

        SendMessageW(lst.Handle, LB_SETTABSTOPS, count + 1, pos)
        ' lst.Refresh()
    End Sub

    Public Declare Unicode Function SendMessageW Lib "user32" ( _
        ByVal hWnd As IntPtr, _
        ByVal wMsg As Int32, _
        ByVal wParam As Int32, _
        ByVal lParam() As Int32) As Int32

    'Public Declare Unicode Function GetDialogBaseUnits Lib "user32" () As Integer

    Public Const LB_SETTABSTOPS As Integer = &H192

End Class

ネットで見つけたコードに手を加えて class に閉じ込めた。
こんな感じ↓で呼ぶ。

xxx.Tabs(lst, 21, 25)
lst.Refresh()

21文字と25文字辺りに揃える。「辺り」と言うのは平均文字幅で決まるから。
等幅フォントならピッタリいくはず。

処で、このタブストップ位置を知るにはどうするの?
TEXTMETRIC 構造体の tmAveCharWidth メンバがそのようだ。
VB グラフィクスに明るくないので Windows SDK の手順でやってみよう。
毎度ネットのお世話になり宣言類をば。

Public Declare Unicode Function GetDC Lib "user32" ( _
    ByVal hwnd As System.IntPtr) As System.IntPtr

Public Declare Unicode Function SelectObject Lib "gdi32" ( _
    ByVal hdc As IntPtr, _
    ByVal hgdiobj As IntPtr) As System.IntPtr

Public Declare Unicode Function GetTextMetricsW Lib "gdi32" ( _
    ByVal hdc As IntPtr, _
    ByRef lptm As TEXTMETRICW) As Integer


< Serializable(), StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode) > _
Public Structure TEXTMETRICW
    Public tmHeight As Int32
    Public tmAscent As Int32
    Public tmDescent As Int32
    Public tmInternalLeading As Int32
    Public tmExternalLeading As Int32
    Public tmAveCharWidth As Int32
    Public tmMaxCharWidth As Int32
    Public tmWeight As Int32
    Public tmOverhang As Int32
    Public tmDigitizedAspectX As Int32
    Public tmDigitizedAspectY As Int32
    Public tmFirstChar As UInt16
    Public tmLastChar As UInt16
    Public tmDefaultChar As UInt16
    Public tmBreakChar As UInt16
    Public tmItalic As Byte
    Public tmUnderlined As Byte
    Public tmStruckOut As Byte
    Public tmPitchAndFamily As Byte
    Public tmCharSet As Byte
End Structure

tmAveCharWidth を手に入れるにも一苦労だ。

' extern "C" {
Dim tm As TEXTMETRICW

Dim hDC As System.IntPtr = GetDC(lst.Handle())
Dim old As System.IntPtr = SelectObject(hDC, lst.Font.ToHfont)
Dim h As Integer = GetTextMetricsW(hDC, tm)
SelectObject(hDC, old)
If h Then
    tm.tmAveCharWidth ...
' }

VB で書くには○カ○カしいので、皮肉を込めてコメントに extern "C" を書いておいた。
まんざら冗談でもなく、可能なら使いたいものだ。
VB グラフィクスを使えば簡素になるかも知れないが、さっさと C 流儀で書いた方が確実、且つ手っ取り早い。

グラフィクス:文字列の描画サイズ

一般的には Graphics.MeasureString で得るらしい。DOBON.NET に色々な方法が載せられている。
んが、多少余白が含まれたり、太字なんかでは正確でないそうな。
また、空白は文字でないとでも思っているのか、デフォでは無視するそうな。
(んじゃ何で空白に文字コード割り当ててるんだ?)

と言う訳で、StringFormat を引数に取る書式を呼ぶ必要有り。
ラベル lbl の1行表示のテキストサイズを測る質素な例:
幅0を指定すれば1行を意味すると思われるが念の為に NoWrap も指定(暗黙的に指定されると説明されてはいるが)。
また、気持ち分、Clip 不要の意思表示で NoClip も加えてみた。
Dispose() しちゃって良いのだろうか等、丸で理解してないが…

Dim fmt As New StringFormat( _
    StringFormatFlags.MeasureTrailingSpaces Or _
    StringFormatFlags.NoClip Or _
    StringFormatFlags.NoWrap)

Dim g As Graphics = lbl.CreateGraphics
Dim size As SizeF = g.MeasureString(lbl.Text, lbl.Font, 0, fmt)
g.Dispose()
' size.Width ...
' size.Height ...

何となく DrawText API に DT_CALCRECT を指定した雰囲気なので、そっちでも試す。

Public Declare Unicode Function DrawTextW Lib "user32" ( _
    ByVal hDC As IntPtr, _
    ByVal lpStr As String, _
    ByVal nCount As Integer, _
    ByRef lpRect As System.Drawing.Rectangle, _
    ByVal uFormat As Integer) As Integer

Public Const DT_CALCRECT = &H400
Public Const DT_NOPREFIX = &H800
Public Const DT_NOCLIP = &H100
Public Const DT_SINGLELINE = &H20

Rectangle は RECT 構造体と互換有り。
ListBox のコード拝借。

Dim rc As System.Drawing.Rectangle
Dim hDC As System.IntPtr = GetDC(lbl.Handle())
Dim old As System.IntPtr = SelectObject(hDC, lbl.Font.ToHfont)
Dim cy As Integer = DrawTextW(hDC, lbl.Text, -1, rc, _
                              DT_CALCRECT Or DT_SINGLELINE)
SelectObject(hDC, old)
' rc.right ...
' rc.bottom ...

グラフィクス:角丸コントロール

Round Button

図形描画ではなくボタンコントロールを角丸化。
色んな処で解説有り、何を今更と思ったがコントロールの見た目を変更する(特に既存を)手法として有用と思い書き留め。
ただ、ボタンにしては味気無く、テキストがセンタリングしてない等、実用には遠い。

さて、コントロールにカスタムな要素を取り入れるにはサブクラスが代表的なんだが、もっと独立性を高められるコーディング方法が有るのを発見。次の2ステップがそれ。

  1. NativeWindow の派生クラスを用意し、そこでペイント処理
  2. 呼び出し側でアサイン

1. はこんな感じ。先にベースの WndProc を呼んでるのは、そうしないといけないコントロールが有った為。必要か否か?

Public Class XXX : Inherits NativeWindow
    Private Const WM_PAINT As Integer = &HF
    Protected target As System.Windows.Forms.Button

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)

        If m.Msg = WM_PAINT Then
            Dim g As Graphics = target.CreateGraphics
            g.FillRectangle(Brushes.MediumVioletRed, 0, 0, target.Width, target.Height)

            Dim fmt As New StringFormat
            fmt.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Show

            g.DrawString(target.Text, target.Font, Brushes.Lime, 0, 0, fmt)
            m.Result = IntPtr.Zero
            Return
        End If
    End Sub
End Class

塗りつぶしとテキスト描画だけなので、これで角丸にはならない。それにはリージョンをセットした。これが中々曲者で後で解説する。
また、呼び出しを簡素化したいので、これらをまとめて Attach 関数と名付けた。

    Public Shared Sub Attach(ByVal src As System.Windows.Forms.Button)
        ' XXX の生成と保存、初期化。
        Dim cls As New XXX
        cls.target = src               ' 参照用にセット。
        cls.AssignHandle(src.Handle)   ' これが大事。
        src.Tag = cls                  ' インスタンスを保存。既に使われているなら他に保存せざるを得ないが…

        ' リージョン設定
        Dim R As New Size(30, 30)
        Dim rc As New Rectangle(0, 0, src.Width, src.Height)
        Dim path As New System.Drawing.Drawing2D.GraphicsPath

        'path.FillMode = System.Drawing.Drawing2D.FillMode.Winding
        path.AddArc(rc.Right - R.Width, rc.Top,               R.Width, R.Height, 270, 90)
        path.AddArc(rc.Right - R.Width, rc.Bottom - R.Heigh,  R.Width, R.Height,   0, 90)
        path.AddArc(rc.Left,            rc.Bottom - R.Height, R.Width, R.Height,  90, 90)
        path.AddArc(rc.Left,            rc.Top,               R.Width, R.Height, 180, 90)
        src.Region = New Region(path)
    End Sub

2. は上記を呼ぶだけ。Button1 がボタン。

XXX.Attach(Button1)
AddArc function

さて、GraphicsPath の AddArc は MSDN サンプルを見て書いたのだが、中々癖がある。

パスとは描画の道筋の意味なのだろう。
矩形なボタンの四隅に円弧を指定し変形させる訳だが、 まず角度。これは時計の3時が基点。なので右下の指定が0°になる。 次に座標。これはどうやら円弧を囲む矩形の左上隅を与えるらしい。赤い点がそれ。
何故そういう指定方法なのか、何処かに解説有った気がするが失念。


コントロールのコンテナ移動

例えば、フォーム直下に有るコントロール ctrl をコンテナ(例えばタブページコントロール等)へ移動するには、次のように書けば良いようだ。
Remove が必要な気がしたのだが、コレクションの Count を見ても減少するので不要なのだろう。

Me.TabPage1.Controls.Add(ctrl)
' Me.Controls.Remove(ctrl)

但し、親ウィンドウが変わる為か、移動後の相対位置・サイズは出鱈目な値になるようだ。再設定しないといけない。
その他の状態は移動前と変わらないように思える。
でも、ListBox を移動させてみるとウィンドウ ハンドルが変化する。WM_DESTROY を受け、項目関係のメッセージ LB_xxx を受けている。移動後の見た目は変わったように見えないが…

フォームの起動まで

適当なフォームで WndProc をオーバーライドして Form_Load まで観た。

Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    Debug.WriteLine(m.ToString())
    MyBase.WndProc(m)
End Sub

Protected Overrides Sub OnHandleCreated(ByVal e As System.EventArgs)
    Debug.WriteLine("OnHandleCreated")
    MyBase.OnHandleCreated(e)
End Sub

Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
    Debug.WriteLine("OnResize")
    MyBase.OnResize(e)
End Sub

初めに OnResize が2回も呼ばれている。
0x127 と 129 は予想。
WM_SHOWWINDOW は既に届いている。

OnResize
OnResize
msg=0x0024 (WM_GETMINMAXINFO)     hwnd=0x9b0882 wparam=0x0 lparam=0x13e5f8 result=0x0
msg=0x0081 (WM_NCCREATE)          hwnd=0x9b0882 wparam=0x0 lparam=0x13e5f0 result=0x0
msg=0x0083 (WM_NCCALCSIZE)        hwnd=0x9b0882 wparam=0x0 lparam=0x13e618 result=0x0
msg=0x0001 (WM_CREATE)            hwnd=0x9b0882 wparam=0x0 lparam=0x13e5e0 result=0x0
msg=0x0127  WM_CHANGEUISTATE ?    hwnd=0x9b0882 wparam=0x3 lparam=0x0 result=0x0
OnHandleCreated
msg=0x000c (WM_SETTEXT)           hwnd=0x9b0882 wparam=0x0 lparam=0x51dd8b8 result=0x0
msg=0x0080 (WM_SETICON)           hwnd=0x9b0882 wparam=0x0 lparam=0x0 result=0x0
msg=0x0080 (WM_SETICON)           hwnd=0x9b0882 wparam=0x1 lparam=0x0 result=0x0
msg=0x000e (WM_GETTEXTLENGTH)     hwnd=0x9b0882 wparam=0x0 lparam=0x0 result=0x0
msg=0x000d (WM_GETTEXT)           hwnd=0x9b0882 wparam=0x6 lparam=0x13ea50 result=0x0
msg=0x007c (WM_STYLECHANGING)     hwnd=0x9b0882 wparam=0xfffffffffffffff0 lparam=0x13f390 result=0x0
msg=0x007d (WM_STYLECHANGED)      hwnd=0x9b0882 wparam=0xfffffffffffffff0 lparam=0x13f390 result=0x0
msg=0x007c (WM_STYLECHANGING)     hwnd=0x9b0882 wparam=0xffffffffffffffec lparam=0x13f390 result=0x0
msg=0x007d (WM_STYLECHANGED)      hwnd=0x9b0882 wparam=0xffffffffffffffec lparam=0x13f390 result=0x0
msg=0x0046 (WM_WINDOWPOSCHANGING) hwnd=0x9b0882 wparam=0x0 lparam=0x13f3d0 result=0x0
msg=0x0083 (WM_NCCALCSIZE)        hwnd=0x9b0882 wparam=0x1 lparam=0x13f3a4 result=0x0
msg=0x0047 (WM_WINDOWPOSCHANGED)  hwnd=0x9b0882 wparam=0x0 lparam=0x13f3d0 result=0x0
msg=0x0018 (WM_SHOWWINDOW)        hwnd=0x9b0882 wparam=0x1 lparam=0x0 result=0x0
msg=0x0210 (WM_PARENTNOTIFY)      hwnd=0x9b0882 wparam=0x87a0001 lparam=0x1e087a (WM_CREATE) result=0x0
msg=0x0055 (WM_NOTIFYFORMAT)      hwnd=0x9b0882 wparam=0x1ed0842 lparam=0x3 result=0x0
msg=0x0129  WM_QUERYUISTATE ?     hwnd=0x9b0882 wparam=0x0 lparam=0x0 result=0x0
msg=0x0210 (WM_PARENTNOTIFY)      hwnd=0x9b0882 wparam=0x10001 lparam=0x1ed0842 (WM_CREATE) result=0x0
msg=0x0210 (WM_PARENTNOTIFY)      hwnd=0x9b0882 wparam=0x85a0001 lparam=0x5b085a (WM_CREATE) result=0x0
msg=0x0210 (WM_PARENTNOTIFY)      hwnd=0x9b0882 wparam=0x90a0001 lparam=0x19090a (WM_CREATE) result=0x0
msg=0x0210 (WM_PARENTNOTIFY)      hwnd=0x9b0882 wparam=0x8580001 lparam=0x340858 (WM_CREATE) result=0x0
msg=0x000e (WM_GETTEXTLENGTH)     hwnd=0x9b0882 wparam=0x0 lparam=0x0 result=0x0
msg=0x000d (WM_GETTEXT)           hwnd=0x9b0882 wparam=0x6 lparam=0x13e24c result=0x0
Form1_Load

エントリポイント

Module Module1
    Public Sub Main(ByVal args As String())
        Application.Run(New Form1)
    End Sub
End Module

オーナーウィンドウ

VB に限る話題ではないが…
日本語訳では「所有されているウィンドウ」となっていた。
古い Win32 API プログラマーズ リファレンス に拠ると次のように書かれてある。

オーナー ウィンドウになれるのは、 オーバーラップ ウィンドウとポップアップ ウィンドウだけです。子ウィンドウはオーナー ウィンドウにはなれません。

CreateWindowEx 関数辺りで仮引数名が hwndParent となっているのがそれ。ちゃんと /* 親ウィンドウまたはオーナー ウィンドウのハンドル */ と書かれてある。

ここで困るのが、ウィンドウ作成した後でオーナーを変更出来そうに無い事。
SetWindowLong なんかで、と思うが、GWL_HWNDPARENT は有効な引数になっていない。
それに次のようにはぐらかした文が書かれている。

子ウィンドウの親を変更するときは、GWL_HWNDPARENT を指定して SetWindowLong 関数を呼び出すのではなく、SetParent 関数を使ってください。

何故?どうして? 子ウィンドウじゃなくて、オーナーを変えるにはどうなのよ? と言いたい(MS の文章ってのは大抵こんなだ orz)…挙句には「保証しません」とか出して来る。
でもやってる人は居るようだ。問題有りとは書かれていない(参考)。

Public Declare Unicode Function SetParent Lib "user32" ( _
    ByVal hWndChild As System.IntPtr, _
    ByVal hWndNewParent As System.IntPtr) As System.IntPtr

移動を阻止する

例えばフォームの移動をスクリーン左端から 100 ピクセル以上に制限するとしたら…
簡単には Left プロパティへそれなりの値をセットすれば良い訳ですが、シビアに考えると一時的に移動してから戻るので面白くない。

ここはひとつ、移動を許さない方法は無いのか?
と SDK ベースで考えていたら WM_WINDOWPOSCHANGING に阻止出来そうな内容が書かれてある。

アプリケーションは、 WINDOWPOS構造体のflagsメンバの適切なビットをセットまたはクリアすることにより、 ウィンドウに対して変更が行われないようにすることができます。

ほほぅ、VB で確かめるにはもどかしいが、やってみる。

Imports System.Runtime.InteropServices

Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    Select Case m.Msg
    Case WM_WINDOWPOSCHANGING
        Dim wp As WINDOWPOS
#If 0 Then
        CopyFromWindowPos(wp, m.LParam, Len(wp))
#Else
        wp = CType(m.GetLParam(wp.GetType()), WINDOWPOS)
#End If
        If wp.flags And SWP.SWP_NOMOVE Then
        ElseIf wp.x < 100 Then
            wp.x = 100
            wp.flags = wp.flags Or SWP.SWP_NOSIZE
#If 0 Then
            CopyToWindowPos(m.LParam, wp, Len(wp))
#Else
            Marshal.StructureToPtr(wp, m.LParam, False)
#End If
            Return
        End If
    End Select

    MyBase.WndProc(m)
End Sub

SWP_NOMOVE が付いてればスルー(移動だけに注目するのだから…)、
限界位置より大きければスルー、
領海侵犯が起こりそうなら SWP_NOMOVE を付加すれば良い。
が、やってみると違和感有るので、限界位置でクリップし、サイズ変更を(許すスタイルの場合の為に)禁止してみた。
ここでは移動かサイズ変更か判らないので二者択一するしか無さそうである。
従って、最大化時に最大化しない orz

定義類はパクり物だが、ここで面倒なのは lParam ポインタと WINDOWPOS 構造体との読み書きする処。
ダイナミックに ByRef なんて出来ないので memcpy する例が多いが、モダン?に見えた書き方にした。
しかし GetLParam は有っても SetLParam は無い。
PtrToStructure も用意されてはいるが、いい加減参照させたらどうか?
何をするにも VB は面倒だ。

    Public Const WM_WINDOWPOSCHANGING As Integer = &H46

    < StructLayout(LayoutKind.Sequential) > _
    Public Structure WINDOWPOS
        Public hwnd As Integer
        Public hwndInsertAfter As Integer
        Public x As Integer
        Public y As Integer
        Public cx As Integer
        Public cy As Integer
        Public flags As SWP
    End Structure

    Enum SWP As Integer
        SWP_NOSIZE = &H1
        SWP_NOMOVE = &H2
        SWP_NOZORDER = &H4
        SWP_NOREDRAW = &H8
        SWP_NOACTIVATE = &H10
        SWP_FRAMECHANGED = &H20
        SWP_SHOWWINDOW = &H40
        SWP_HIDEWINDOW = &H80
        SWP_NOCOPYBITS = &H100
        SWP_NOOWNERZORDER = &H200
    End Enum

    Public Declare Sub CopyFromWindowPos Lib "kernel32" Alias "RtlMoveMemory" ( _
        ByRef dst As WINDOWPOS, _
        ByVal src As IntPtr, _
        ByVal cBytes As Int32)

    Public Declare Sub CopyToWindowPos Lib "kernel32" Alias "RtlMoveMemory" ( _
        ByVal dst As IntPtr, _
        ByRef src As WINDOWPOS, _
        ByVal cBytes As Int32)

WM_WINDOWPOSCHANGING を見ているだけでは最大化しようとしているのかどうか判りそうに無い。
WM_SYSCOMMAND メッセージで SC_MAXIMIZE コマンドを記憶しておいて、その内に WM_WINDOWPOSCHANGING がやって来る、と考えるか…

尤も、WM_GETMINMAXINFO でセットするのが賢明。
なお、ptMaxSize なんかの値にタスクバーのサイズは含まれてないようだ。
良くワカラン…

動的アセンブリロード

DLL でなく EXE のままでも関数呼び出しは可能、と何処かで見かけた。
ほぅ、そうですかい、ではやってみよう。
単純なフォームクラスに自身を表示する Shared 関数を追加する。
おまけで Owner を渡す。これを hoge.exe とする。

Public Class VBexe : Inherits System.Windows.Forms.Form
    Public Shared Sub Exec(ByRef owner As System.Windows.Forms.Form)
        Dim frm = New VBexe
        frm.Owner = owner
        frm.Show()
    End Sub
    ...

Load ではなく LoadFile や LoadFrom でないと失敗する。
これで呼び出した方のプロセス、スレッドでフォームが表示される。

Try
    Dim asm As System.Reflection.Assembly = _
               System.Reflection.Assembly.LoadFile( _
                "C:\...\hoge.exe")
    Dim myType As Type = asm.GetType("hoge.VBexe")
    Dim myMethod As MethodInfo = myType.GetMethod("Exec")

    Dim obj As Object = Activator.CreateInstance(myType)
    myMethod.Invoke(obj, New Object() {Me})

Catch e As Exception
    Debug.WriteLine(e)
End Try

AppDomain

アプリケーション内で共有できる仕組みが無いか探していた処で見つけた。
プロセスとも違う、もやもやした単位。

これに名前付けて保持できるそうな。
フォーム(Me)をセットしてみたが、特に問題は無い。

System.AppDomain.CurrentDomain().SetData("hoge", Me)

Dim o As Object = System.AppDomain.CurrentDomain().GetData("hoge")

さて、ExecuteAssembly なるものが有ったので、これで現在のドメインから起動してみると…

System.AppDomain.CurrentDomain.ExecuteAssembly( _
    "C:\...hoge.exe")

これは駄目、
'System.InvalidOperationException' のハンドルされていない例外が system.windows.forms.dll で発生しました。
追加情報 : 1 つのスレッド上で、2 つ目のメッセージ ループを開始できません。
Application.RunDialog または Form.ShowDialog を代わりに使用してください。

では新しくドメインを作成したらどうか?
これは起動は出来るが、終了時に問題を起こした。

System.AppDomain.CreateDomain("Hoge").ExecuteAssembly( _
    "C:\...hoge.exe")

vb1.exe の 0x7c959af2 で初回の例外が発生しました : 0xC0000005: 場所 0x00000010 に書き込み中にアクセス違反が発生しました。

う~む、ウィンドウ メッセージ ループが駄目か…

DLL の共有セグメント

VB.NET とは無縁の古典的な DLL の話。C それも VC 固有かも知れない。
昔、そんなテクニックが有ったのを思い出し、MSDN の説明を見て試す。
2007年に更新されたままだが、有効なようだ。

ソース中の共有データ
//#pragma comment(linker, "/SECTION:.hoge,RWS!K")
#pragma data_seg(".hoge")
char hogestr[64] = "hogeHOGE";
#pragma data_seg()

リンカの追加オプション
/SECTION:.hoge,RWS

追加のオプション枠に例のような文字列で指定する。
名前に、ドットで始めねばならない規則は無かったと思うが…

また、オブジェクト ファイルにリンカ オプションを記録する事になっている pragma comment もいけそう。複数箇所に現れうるので嫌がる人も居そうな反面、好む人も居る。

なお、単純サンプルだと期待通りに動作するが、複雑巨大システムだとうまくいかなかったりする。何か制限が有るのか、他の原因なのか判らない… その場合はメモリマップド ファイルの方が簡単だったりする。

さて、オプションが狙い通りにセットされたかどうか?
VS 付属の dumpbin で見てみる。
dumpbin xxx.dll /headers または
dumpbin xxx.dll /section:.hoge

Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file xxx.dll

File Type: DLL

SECTION HEADER #6
   .hoge name
     14C virtual size
   3A000 virtual address (1003A000 to 1003A14B)
    1000 size of raw data
   28000 file pointer to raw data (00028000 to 00028FFF)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
D0000040 flags
         Initialized Data
         Shared
         Read Write
...

flags の処に Shared 属性が付いている。良さそうだ。
なお、K(Cacheable 属性)は「セクションをキャッシュ不可としてマークします。」と有るが、これは良く説明を読まねばならない。
/SECTION:.hoge,RWS!K のように否定でもって指定しないと意味が無い。
そうすれば Not Cached となる。尤も、どういう場合に効果有るのか判らない…

Paint からのクローズ

うまくゆかない。
とあるフォームを Paint イベントで(何か処理させた後)クローズする。

Public Class yyy : Inherits System.Windows.Forms.Form
    Private Sub yyy_Paint(...
        ...
        Me.Hide()
        'Me.Visible = False
        Me.Close()
    End Sub
End Class

これをモードレス表示させる(モーダルでは起こらない)。

Public Class xxx : Inherits System.Windows.Forms.Form
    Public f As yyy
    ...
    Private Sub Button1_Click(...
        f = New yyy
        f.Show()
    End Sub
End Class

するとクローズが終わる頃、次の例外が起こる。

'System.ObjectDisposedException' のハンドルされていない例外が system.windows.forms.dll で発生しました。 追加情報 : "yyy" という名前の、破棄されたオブジェクトにアクセスできません。

何故 モーダルで起こらないのか違いを見ると Me.Close() 後に Dispose が呼ばれないことが判る。
Close() でインスタンスまで破棄しようとするモードレスの振る舞いがいけない。(何て作りだ。生成時は New させて Show を強いるくせに…)

そう言う訳でモードレスの場合には Close() を呼ぶ時期を別にしないといけない。
Activated イベントで処理したり、API PostMessage 利用する手があるが、適切なタイミングが無ければ放置してガベージコレクタに任せるのが良いだろう。

それにしても Close() で Dispose を呼んでいるとは無茶苦茶な…
今時このような同じキーワードで異なる振る舞いを定義するのは如何なものか? 何を考えているのか首を傾げる。

Tips

Dim の省略

Dim sum As Integer = 0
For i As Integer = 0 To 10
    sum += i
Next

早見表とか Win32 代替関数とか

何処かに一覧が載っていた気がするが失念。

参考


Home / VB.NET

© 2008 usskim    http://usskim.web.fc2.com/
inserted by FC2 system