Visual Basic .NETに関するメモ

このページは、不定期に更新されています。更新箇所の明示は行われませんのでご注意ください。また、内容はごく一時的なメモに類するものです。一貫性や真偽について、保証できませんので、ご了承ください。

また、こちらにいくつかのサンプルコードを掲載していますので、あわせてご利用ください。


浮動小数点数の小数部をインラインで切り落とす

Single型あるいはDouble型の変数に格納された浮動小数点数について、小数部だけを単純に切り落とす方法として、Math.Truncate関数があるが、メソッドの呼び出しのためのオーバーヘッドが大きい。また、C、C++、C#では、キャストにより整数型の変数に変換する際には、少なくとも正の値であれば、単純に小数部が切り落とされるが、Visual BasicのCIntやCLngによる変換では、複雑な丸め操作を伴う。

ここでは、浮動小数点数から整数へ小数部を切り飛ばすことによって変換する方法で、可読性には欠けるがインラインで記述・展開できる方法を示す。

Dim i as Long
Dim d as Double
i = CLng(Math.Truncate(d))

に代わる方法である。以下のような構造体を用いたトリッキーな方法であるが、要するに、Double型変数とLong型あるいはInteger型の変数をメモリ上で同じアドレスを共有するように工夫し、Double型変数の内容を整数型変数を通じてbit単位で操作する。

'構造体のメンバー変数としてDouble型とLong型(共に同じ
'8Byte長)をメモリ上の同じ位置を共有させることができる。
'同様に、メモリ上の一部を4Byte長のInteger型で共有させる。
'CやC++で言うところの共用体(union)と同等である。

'この機能は.Net Frameworkの以下の名前空間で提供されている。
Imports System.Runtime.InteropServices

'属性を用いてメンバー変数のメモリ上の位置を細かく指定できる。
'詳しくは、.Net Frameworkのドキュメントを参照のこと。
<StructLayout(LayoutKind.Explicit)> _
Private Structure DU
    <FieldOffset(0)> Public D As Double
    <FieldOffset(0)> Public L As Long
    <FieldOffset(4)> Public I4 As Integer

    Public Const FracBitLength As Integer = 52I
    'ビットシフトの方向が逆であったものを修正(2008/2/19)
    Public Const ImplicitBit As Long = 1L << FracBitLength
    Public Const ExplicitTopBit As Long = ImplicitBit \ 2
    Public Const GetFrac As Long = ImplicitBit - 1
    Public Const GetSgn As Long = &H8000000000000000L
    Public Const GetSgnFrac As Long = GetSgn Or GetFrac
End Structure

まず、Long型の変数へ変換する方法を示す。

'変換操作を行うDUの変数を宣言する。
Dim f As DU
'変換結果を代入する変数を宣言する。
Dim i As Long
'DUのメンバー変数Dに変換したい値を代入する。
f.D = ...

'操作の基本的な考え方は、浮動小数点数の指数部を読み取って仮数部の
'中で小数点の位置を求め、仮数部のデータを右にシフトして小数点以下
'の数値を追い出してしまうことである。

'指数部の読み取り。
'f.I4 >> 20          により、指数部のデータを取り出す。
'これを1023から引いて     仮数部の中での小数点の位置をbit単位で得る。
'                         (最上位bitから数えてマイナスを付した値)
'仮数部のbit長52を足して   必要なシフト量sdを得る。
Dim sd As Integer = 1075 - (f.I4 >> 20)
If sd > DU.FracBitLength Then
    'sdが仮数部のbit長より大きければ、1未満の小数であり、ゼロを返す。
    i = 0
ElseIf sd < 0 Then
    'sdが負の場合、すべての桁の値を仮数部の精度では表せないような
    '大きな数値である。そのまま、CLngで変換して代入すればよい。
    'sd < 0を考慮する必要がなければ、このElseIfブロックは不要。
    i = CLng(f.D)
Else
    '仮数部のみを取り出してシフト操作を行う。
    'f.L And DU.GetSgnFrac     により、符号と仮数部をすべて取り出し、
    'Or DU.ImplicitBit         により、浮動小数点数の場合は「暗黙」に存在が
    '                          仮定されているbitを明示的にセットし、
    '>> sd                     により1の位が最下位bitになるようにシフトする。
    '                          Long型であれば、符号部は保存される。
    i = ((f.L And DU.GetSgnFrac) Or DU.ImplicitBit) >> sd
End If

Integer型の変数へ変換する場合は、上述のElseブロック内の代入文の右辺全体をCIntで囲めばよい。

ここではコメントを付して示したので行数が多くなっているが、構造体DUの宣言、作業用と代入用の値の宣言を除けば、シフト量sdの計算に2つの演算、sdの比較と分岐、及び仮数部の取り出しとシフト操作に3つの演算しか行っておらず、Math.Truncate関数の呼び出しのオーバーヘッドと実際の演算、及びCLngの変換操作に費やす演算量(命令数)に比べればはるかにコンパクトなオブジェクトコードになり、高速に実行される。

整数型の変数に変換せず、Double型のままで小数部を切りとばす、すなわち、Math.Truncateと全く同じ機能をインラインで記述し、f.Dの値を上書きするには、上述のsdの算出までは同様で、引き続いて、以下のとおり処理すればよい。

If sd > DU.FracBitLength Then
    f.D = 0.0#
ElseIf sd >= 0 then
    '1L << sd    により、1に0がsd個続く2進数を用意し、
    '-1          により、1がsd個続く値にして、
    'Not         で反転して、下位のsd bitが0で、残りの上位bitが
    '            1であるマスク用のデータを生成する。
    f.L = f.L And (Not ((1L << sd) - 1))
End If
'sd < 0 の時は、小数点以下の位はない大きな値なので、何もしない。

System.Randomクラスの実装の.Net Frameworkバージョン間の差異

System.Randomクラスの実装は、.Net Frameworkのバージョン1.1と2.0の間で異なっている。このことは、ドキュメントには明記されていないので注意を要する。特に、バージョン2.0のドキュメントの、Sampleメソッドをオーバーライドする導出クラスのサンプル・プログラムは、バージョン1.1の実装による実行結果を示しており、誤っている。以下に詳細を示す。

Randomクラスは乱数列を生成する手法として、Knuthの減算法を用いている。このことはドキュメントにも明記されており、バージョン1.1と2.0で共通である。内部では、基となる数値として、31bit整数により乱数を生成している。バージョン2.0では、この31bit整数をNext(引数なし)関数で返している。また、NextBytesメソッドでは、Byte型配列の各要素ごとに、31bit整数の下位8bitを格納している。

次に、この31bit整数に定数4.6566128752458E-10を乗ずることによりDouble型乱数を得ている。この操作がSampleメソッドで行われており、SampleメソッドをOverrideしてこのDouble型乱数の分布に変更を加えた導出クラスにおいては、これより下流の結果に影響が及ぶ。

Double型乱数は、バージョン1.1と2.0ともにNextDouble関数で取得することができる。

バージョン1.1では、Next(引数なし)関数で返される31bit長の整数型乱数は、NextDoubleで得られる乱数を定数4.6566128752458E-10で除したものである。また、NextBytesメソッドでは、Byte型配列の各要素ごとに、この31bit整数の上位8bitを格納している。

バージョン1.1と2.0ともに、Next(上限値)とNext(下限値、上限値)の関数は、NextDoubleで得られる乱数を適当にスケーリングして、また、オフセット値を加え、小数部を切り飛ばした結果を返している。

まとめると

バージョン1.1では、SampleメソッドをOverrideした導出クラスでは、Next(すべてのオーバーロード)、NextDouble、NextBytesのすべての関数・メソッドに影響が及ぶ。しかし、バージョン2.0では、Next(引数なし)関数とNextBytesメソッドには影響は及ばず、Next(上限値)、Next(下限値、上限値)及びNextDoubleの関数に影響が及ぶ。

バージョン2.0のサンプル・プログラムでは、Next(引数なし)関数にもSampleメソッドのOverrideの影響が及ぶような結果を示しているが、誤りである。

Randomクラスそのものを用いる場合であっても、バージョン1.1と2.0と比較すると、初期値を同じ値にした場合でも、NextBytesメソッドで得られる結果は異なる。その他の関数の結果は同じになる。


MDIを使わずに複数のFormをアプリケーションごとにまとめて扱う

MDIを用いると、IsMdiContainerプロパティーがTrueである「親」Formの中に、複数のFormをまとめることができる。しかし、以前のVisual Basicの開発環境のように、MDIを使わない方が、他のアプリケーションやウィンドウが隠れず、便利なこともある。

MDIを用いず、かつ、一つのアプリケーションに属する複数のFormがひとまとまりになって、他のアプリケーションのウィンドウに対して画面上の前後関係を維持できるようにするためには、FormのOwnerプロパティーを用いる。

この場合も「親」になるFormを一つ決める。例えば、frmMotherとする。次に、ひとまとめにして扱いたいFormを、frmChild1、frmChild2とする。

frmChild1.Owner = frmMother
frmChild2.Owner = frmMother

このように設定すれば、frmMotherと一体になって、frmChild1とfrmChild2はひとまとまりで、他のアプリケーションのウィンドウと前後関係を維持する。

さらに、「親」であるfrmMotherは実質的に不要で、対等な関係にあるfrmChild1とfrmChild2だけがユーザーインターフェイスに必要な場合には、次のとおりfrmMotherのプロパティーを設定し、frmChild1とfrmChild2を表示する。

Private Sub frmMother_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
                           Handles MyBase.Load
    'アプリケーションを代表するアイコンをセットする。
    '「Alt + Tab」キーの操作で現れるアプリケーション一覧には、まず、このアイコン
    'が表示される。ただし、タスクマネージャーのアプリケーションタブの一覧には
    'このアイコンは表示されない。デザイン時にプロパティーウィンドウで設定可。
    Me.Icon = ...

    '「親」のShowInTaskbarはTrueにする。Falseでは、上記のアプリケーション一覧に
    '正しくアイコンが表示されない。デザイン時にプロパティーウィンドウで設定可。
    Me.ShowInTaskbar = True

    '「親」のWindowStateは、Normalにする。Minimizeではタスクバーに余分な表示が
    '現れてしまう。デザイン時にプロパティーウィンドウで設定可。
    Me.WindowState = FormWindowState.Normal

    'さらに、frmMotherのLoadイベントで、frmChild1、frmChild2を表示する。
    frmChild1.Show
    frmChild2.Show
End Sub

frmMotherのActivatedイベントで、自分自身を表示されないように隠す。

Private Sub frmMother_Activated(ByVal sender As Object, ByVal e As System.EventArgs) _
                                Handles MyBase.Activated
    Me.Visible = False
End Sub

FormよりPictureBoxのほうが描画が速い?

簡単なグラフィックス描画の練習をしていて気付いたが、Form上に描画するのに比べて、FormにPictureBoxを配置してPictureBox上に描画するほうが動作が速いようである。再描画を連続的に繰り返すような場合の「チラツキ」がPictureBoxのほうが断然少なく、スムーズな「動き」が表示できる。

これは、FormやPictureBoxの親クラスであるControlの「スタイル」に依存している。Controlの「スタイル」はSetStyleメソッドで設定できるが、このメソッドはProtectedである。VB.NETでFormを使うときは、Fromを継承した独自のFormクラスを作るので、この中でSetStyleメソッドを呼び出して、ControlStylesのUserPaint、AllPaintingInWmPaint、DoubleBufferを設定すれば、FormもPictureBoxと同様にスムーズな描画が行える。


ピクセルごとの描画

画像を描画することのできるコントロールには、FormやPictureBoxがあり、描画が必要なとき(例えば、別のウィンドウの背後に隠れていたものが前面に出た、あるいは、覆っていたウィンドウが別の場所に移動した)にはPaintイベントが発生する。

PaintイベントではSystem.Windows.Forms.PaintEventArgsクラスのパラメータが取得でき、このパラメータのGraphicsメンバーを使って描画を行う。GraphicsメンバーはSystem.Drawing.Graphicsクラスのオブジェクトで、各種図形を描画する機能がメソッドとして提供されているが、ピクセルごとに点を描画するメソッドは提供されていない。

Graphicsメンバーに点を直接描画することができないことは戸惑い驚くが、次のような代替策がある。

System.Drawing.GraphicsクラスのDrawImageメソッドによってSystem.Drawing.Imageクラスのオブジェクトを描画することができる。System.Drawing.Imageクラスから派生したSystem.Drawing.Bitmapクラスがあり、System.Drawing.Bitmapクラスのオブジェクト上ではピクセルごとに描画を行うことができる。

つまり、System.Drawing.Bitmapクラスのオブジェクトに点を描き込んだ上で、このオブジェクトをGraphicsメンバーに描画すればよい。

これらをまとめて、ピクセルごとの描画を行うコードの例を示す。(PictureBox1コントロールのPaintイベントを想定する。) 描画する領域のサイズが大きくなるとパフォーマンスの低下が目立つが、この方法であれば、.Net Frameworkの仕様の範囲ですべて処理できる。

Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As System.Windows.Forms.PaintEventArgs) _
                              Handles PictureBox1.Paint
    '--------------------------------------------------------------------
    '(1) まず、e.Graphicsの描画メソッドを使って、最も背景を描画する。
    'e.Graphics.Draw.... など
    '--------------------------------------------------------------------
    '(2) 「点」を描く

    'PictureBox1の描画領域の大きさを取得する。
    Dim Width As Integer = PictureBox1.ClientSize.Width
    Dim Height As Integer = PictureBox1.ClientSize.Height

    'PictureBox1の描画領域と同じ大きさのBitmapを用意する。
    'このとき、Bitmapは、ARGBフォーマットで、すべてのピクセルが
    'A=0、R=0、G=0、B=0に、つまり「透明(A=0)」に初期化される。
    Dim bmpImage As New System.Drawing.Bitmap(Width, Height)

    'Bitmapに点を描く。描き込んだ「点」は、A<>0(不透明)であることがミソ
    'bmpImage.SetPixel.... など

    '点を描き込んだBitmapを描画する。描き込んだところは不透明、
    '描き込んでいないところは透明なので、(1)で描いた図形は、透けて
    '見える。
    e.Graphics.DrawImage(bmpImage, 0, 0)

    'bmpImageは不要なので廃棄する。
    bmpImage.Dispose()
    '--------------------------------------------------------------------
    '(3) 最も前面を描画する。
    'e.Graphics.Draw.... など
    '--------------------------------------------------------------------
End Sub

Bitmapのサイズが大きくなるとパフォーマンスが低下する。Windows APIを利用するとパフォーマンスを改善できるが、.Net Frameworkの仕様の範囲を逸脱する。またARGBフォーマットに対応しないので、不透明さ「A」の設定ができない。Windows APIを利用する例を示す。

'Windows APIを利用するための宣言
Declare Auto Function SetPixel Lib "gdi32" (ByVal hdc As IntPtr, _
                                            ByVal X As Integer, _
                                            ByVal Y As Integer, _
                                            ByVal Color As Integer) As Boolean

Private Sub PictureBox1_Paint(ByVal sender As Object, _
                              ByVal e As System.Windows.Forms.PaintEventArgs) _
                              Handles PictureBox1.Paint
    '--------------------------------------------------------------------
    '(1) まず、e.Graphicsの描画メソッドを使って、最も背景を描画する。
    'e.Graphics.Draw.... など
    '--------------------------------------------------------------------
    '(2) 「点」を描く。

    '     ハンドルの取得から解放の間は、e.Graphics.Draw...のような
    '     GDI+の描画メソッドは使ってはならない。

    'PictureBox1のハンドルを取得する。
    Dim hdc As IntPtr = e.Graphics.GetHdc()

    'System.Drawing.Colorのオブジェクトを、ColorTranslatorクラスを使って
    'Windows APIで使える整数型のデータに変換する。
    '不透明さ「A」の設定は無視される。
    Dim PlotColorWin32 As Integer = ColorTranslator.ToWin32(PlotColor)

    '点を描き込む。
    'SetPixel(hdc, x, y, PlotColorWin32)など

    'ハンドルを解放する。
    e.Graphics.ReleaseHdc(hdc)
    '--------------------------------------------------------------------
    '(3) 最も前面を描画する。
    'e.Graphics.Draw.... など
    '--------------------------------------------------------------------
End Sub