独自メッセージボックス
特に更新するネタも無かったので放置してました。
相変わらずWinFormプログラムばかり書いています。
Windows標準のメッセージボックスはOS標準のフォントサイズで表示されるのですが、最近はタブレットやタッチパネル用途のプログラムを作る事もあり、文字が小さくて読みにくかったり、指で操作するにはボタンが若干小さいように感じられました。
メッセージボックスを使わないというのも一つの選択肢ですが、それでもまだ使いどころはあると思うので、自前でカスタマイズ可能なメッセージボックスを作成してみました。
標準のメッセージボックス(Win10)
独自メッセージボックス(Win10、Meiryo UI 24pt)
デモプログラムを用意しました。
https://1drv.ms/u/s!AiiVDEkK6bacgWbGsaAwQi6KPaPU
標準的なメッセージボックスの機能は一通り実装しつつ、フォントサイズをBaseFontプロパティで設定出来るようにしています。ただし、PanelやLabelを組み合わせて作成している事もあり、OS標準のWindowの外観とは若干異なります。
(以下は只の愚痴です)
Windowsのバージョンが変わる度にAero、Modern UI、アクセントカラー、ダークモードなど標準のウィンドウ外観の変更が続き、確かに見栄えは良くなってるのかもしれませんが、ドキュメント化されていない仕様が多く、単純な外観のアプリじゃない場合は気にしないといけない事が増えすぎて戦々恐々としています。WinFormを投げ捨てるべきかと悩む今日この頃。
ネイティブDLLやCOMを使う
.NET環境に移行しても、昔からあるネイティブDLLやCOMを利用したくなる事が時々ありますが、その場合に知っていたほうが良いこと等を書いていきます。
(1) 文字列の受け渡し
CharSetを適切に指定すれば、String型でやりとり可能です。WinAPIでAnsi版、Unicode版両方がある場合でも自動的に解釈してくれます。(紛らわしいので、明示的に指定するに越したことはないのですが。)
'この例だとUnicode版のLoadLibraryWを呼び出してくれる Public Shared Declare Unicode Function LoadLibrary Lib "kernel32" (lpFileName As String) As IntPtr
(2) 構造体のアドレスを渡す場合
LayoutKind.Sequentialで宣言した構造体の参照を渡してあげれば、勝手にマーシャリングしてくれます。2byte以下のメンバを含む構造体はPack値に注意しましょう。
Imports System Imports System.Runtime.CompilerServices Imports System.Runtime.InteropServices Public Class Form1 <StructLayout(LayoutKind.Sequential, Pack:=4)> Private Structure RECT Public left As Integer Public top As Integer Public right As Integer Public bottom As Integer End Structure Private Declare Auto Function GetDesktopWindow Lib "user32.dll" () As IntPtr Private Declare Auto Function GetWindowRect Lib "user32.dll" (ByVal hwnd As IntPtr, <Out()> ByRef rc As RECT) As Integer Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click Dim rc As New RECT GetWindowRect(GetDesktopWindow(), rc) MsgBox(rc.right & " " & rc.bottom) End Sub End Class
(3) COMインターフェースのポインタを受け取る場合
こちらも、COMインターフェースの宣言をきちんとしてあげれば、インターフェースの参照を渡してあげればOKです。Object型で受けて後からキャストすることも可能です。下は、IMallocでのメモリ確保&解放の例です。
Imports System Imports System.Runtime.CompilerServices Imports System.Runtime.InteropServices Public Class Form1 <ComImport()> _ <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _ <Guid("00000002-0000-0000-C000-000000000046")> _ Private Interface IMalloc <MethodImpl(MethodImplOptions.PreserveSig)> Function Alloc(ByVal cb As UInt32) As IntPtr <MethodImpl(MethodImplOptions.PreserveSig)> Function Realloc(ByVal pv As IntPtr, ByVal cb As UInt32) As IntPtr <MethodImpl(MethodImplOptions.PreserveSig)> Sub Free(ByVal pv As IntPtr) <MethodImpl(MethodImplOptions.PreserveSig)> Function GetSize(ByVal pv As IntPtr) As UInt32 <MethodImpl(MethodImplOptions.PreserveSig)> Function DidAlloc(ByVal pv As IntPtr) As UInt32 <MethodImpl(MethodImplOptions.PreserveSig)> Sub HeapMinimize() End Interface Private Declare Sub CoGetMalloc Lib "ole32.dll" (ByVal dwMemContext As Integer, <Out()> ByRef malloc As IMalloc) Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click Dim malloc As IMalloc = Nothing CoGetMalloc(1, malloc) Dim ptr As IntPtr = malloc.Alloc(1000) Dim size As UInt32 = malloc.GetSize(ptr) MsgBox("PtrAddress:0x" & Hex(ptr.ToInt64) & " PtrSize:" & size) malloc.Free(ptr) size = malloc.GetSize(ptr) Marshal.ReleaseComObject(malloc) malloc = Nothing End Sub End Class
(4) コールバック関数(関数ポインタ)を指定する場合
コールバック関数と同じ引数を持つDelegateを作成して渡せばOKです。Marshal.GetFunctionPointerForDelegateを使えば、関数ポインタも取得出来ます。下は、EnumChildWindowsの使用例です。
Imports System.Collections.Generic Imports System.Runtime.InteropServices Public Class EnumWindowSample Private Delegate Function EnumWindowsDelegate(hWnd As IntPtr, lparam As IntPtr) As Boolean Private Declare Function EnumChildWindows Lib "user32.dll" (ByVal hWnd As IntPtr, ByVal lpEnumFunc As EnumWindowsDelegate, ByVal lparam As IntPtr) As Integer Private Declare Function EnumChildWindows Lib "user32.dll" (ByVal hWnd As IntPtr, ByVal lpEnumFunc As IntPtr, ByVal lparam As IntPtr) As Integer Private _ListHWnd As New List(Of IntPtr) 'Delegateを渡す Public Function GetChildWindows1(ByVal hWndOwner As IntPtr) As List(Of IntPtr) _ListHWnd.Clear() EnumChildWindows(hWndOwner, New EnumWindowsDelegate(AddressOf EnumWindowsProc), IntPtr.Zero) Return _ListHWnd End Function '関数ポインタを取得して渡す Public Function GetChildWindows2(ByVal hWndOwner As IntPtr) As List(Of IntPtr) _ListHWnd.Clear() Dim pEnumWindowsProc As IntPtr = Marshal.GetFunctionPointerForDelegate(New EnumWindowsDelegate(AddressOf EnumWindowsProc)) EnumChildWindows(hWndOwner, pEnumWindowsProc, IntPtr.Zero) Return _ListHWnd End Function 'ウィンドウ列挙コールバック関数 Private Function EnumWindowsProc(hWnd As IntPtr, lParam As IntPtr) As Boolean _ListHWnd.Add(hWnd) Return True End Function End Class
WPFという選択肢
さて、ここまでWinFormsで色々載せておきながら、半透明や見た目がリッチなアプリを簡単に作る方法があります。それがWPFです。
WPFのコントロールだと、Opacity指定するだけで、こんな簡単に半透明化出来ちゃいます。WPFは、Windows標準のコントロールではなく、独自でコントロールの描画を行っているので可能なのだと思います。(Spy++等でWPFアプリを確認すると判りますが、WPFのコントロールはWindowハンドルを持っていません)
ただ、フォームのデザインはxamlになり、イベント処理などもWinFormsとは若干違っているので、既存のWinFormsの資産の流用は難しいので、その辺が中々つらいところではあります。新規で見た目が凝ったアプリ作りたい場合は、一考の余地があるかもしれません。
LabelコントロールのAutoSizeの初期値をFalseにする方法
LabelコントロールのAutoSizeプロパティは、何故かデフォルト値がFalseに設定されているにも関わらず、デザイナー画面で貼り付るとTrueに設定されてしまいます。
Labelを継承したコントロールで、コンストラクタでAutoSize=Falseにすれば簡単に出来るんじゃねとか思ってたのですが、どうも勝手にデザイナー画面がTrueに設定してしまうようです。AutoSizeをオーバーライドして色々やってみたりもしたのですが、やはりデザイナー画面で勝手にTrueにされてしまう現象は変わりませんでした。困ったものです。
最終的には、オーバーライドするのをやめて、独自にAutoSizeプロパティを定義し、InitLayoutで基底クラスのAutoSizeを上書きするという強引な方法で、何とか期待通りの動きになりました。
Private _AutoSize As Boolean = False <DefaultValue(GetType(Boolean), "False")> _ Public Overridable Shadows Property AutoSize As Boolean Get Return _AutoSize End Get Set(value As Boolean) _AutoSize = value MyBase.AutoSize = value End Set End Property Protected Overrides Sub InitLayout() MyBase.InitLayout() MyBase.AutoSize = _AutoSize End Sub
半透明なコントロールを作成する
通常、コントロールは透過色をサポートしていませんが、コントロール作成時にいくつか設定を行うことで透過色を使うことが出来るようになります。
https://1drv.ms/u/s!AiiVDEkK6bacgVnsGluZPiwBHy98
デモプログラムを用意してみました。
下のスライダーを動かすと、Label背景の透過率が変わります。α値0で、完全に背景が透明になります。
さて、どうやって半透明を実現しているかを解説していきます。
以下、TransparentLabelクラスのソースです。
''' <summary> ''' α値を設定可能なLabelコントロール。 ''' </summary> ''' <remarks></remarks> Public Class TransparentLabel Inherits Label Private _BackAlpha As Integer = 0 '背景のα値(0~255、0で透明) ''' <summary> ''' 背景のα値を取得または設定します。 ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> <System.ComponentModel.DefaultValue(GetType(Integer), "0"), _ System.ComponentModel.Description("背景のα値を取得または設定します。(0~255)")> _ Public Property BackAlpha As Integer Get Return _BackAlpha End Get Set(value As Integer) If value < 0 Then value = 0 ElseIf value > 255 Then value = 255 End If _BackAlpha = value Me.Redraw() End Set End Property ''' <summary> ''' 再描画を行う。 ''' </summary> ''' <remarks></remarks> Public Sub Redraw() If _BackAlpha = 255 Then Me.Invalidate() Else '半透明or透明の場合、親が先に描画される必要があるため、親をInvalidate If Not IsNothing(Parent) Then Parent.Invalidate(New Rectangle(Me.Left, Me.Top, Me.Width, Me.Height), True) End If End If End Sub Public Sub New() Me.SetStyle(ControlStyles.UserPaint, True) 'コントロールを独自描画する Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True) 'α値を有効にする Me.SetStyle(ControlStyles.Opaque, True) '背景自動描画OFF Me.SetStyle(ControlStyles.OptimizedDoubleBuffer, False) 'Falseにしないと表示がおかしくなる End Sub Protected Overrides ReadOnly Property CreateParams As System.Windows.Forms.CreateParams Get 'WS_EX_TRANSPARENT 'このスタイルで作成されたウィンドウは透明になります。 'このスタイルで作成されたウィンドウは、そのウィンドウの下にある '兄弟ウィンドウがすべて更新された後にだけ、 WM_PAINT メッセージを受け取ります。 Const WS_EX_TRANSPARENT As Integer = &H20 Dim params As CreateParams = MyBase.CreateParams params.ExStyle = params.ExStyle Or WS_EX_TRANSPARENT Return params End Get End Property Protected Overrides Sub OnPaint(e As System.Windows.Forms.PaintEventArgs) If _BackAlpha > 0 Then 'α値を設定して背景を描画 Dim argb As Integer = (Me.BackColor.ToArgb And &HFFFFFF) Or (Me._BackAlpha << 24) Using br As New SolidBrush(Color.FromArgb(argb)) Dim g As Graphics = e.Graphics g.FillRectangle(br, g.VisibleClipBounds) End Using End If '基底クラスOnPaint呼び出し(文字を書いてもらう) MyBase.OnPaint(e) End Sub End Class
全体の流れは、
①コントロールスタイルの設定(New)
↓
②ウィンドウスタイルの設定(CreateParams)
↓
③描画イベントで、α値を設定してコントロールを描画(OnPaint)
といった感じです。
半透明を実現するキモの部分は、CreateParamsをオーバーライドして、ウィンドウスタイルにWS_EX_TRANSPARENTを追加している所です。
背景色にα値を設定しても、他コントロールが描画される際にはα値を考慮してくれないため、他コントロールが先に描画される必要があるのですが、WS_EX_TRANSPARENTをウィンドウスタイルに設定することによって、自分が最後に描画されるようになります。
注意点としては、WS_EX_TRANSPARENTを設定したコントロール同士が重なった場合、どちらが先に描画されるか判らなくなるため、描画順がおかしくなります。
この画像の例では、デザイナ画面では黄色のラベルを最前面に設定しているのですが、青色のラベルが黄色の手前に表示されているのが判ると思います。WS_EX_TRANSPARENTを設定したコントロール同士は、重ねないようにしてください。
コントロールを半透明に見せる方法は色々ありますが、OSレベルで正式にサポートされている訳では無いので、色々不具合があるのが現状で、完全な解決策は
今のところありません。
プロパティの既定値を設定する
カスタムコントロールに独自のプロパティを実装しておくと、Visual Studioのデザイナ画面で値を設定出来るようになって便利です。今回は、フォーカス時に背景色を黄色(LemonChiffon)に変更するTextBoxを考えてみましょう。
Public Class TextBoxEx Inherits TextBox 'フォーカス無し背景色 Private _NoFocusBackColor As Color = SystemColors.Window Protected Overrides Sub InitLayout() MyBase.InitLayout() '元の背景色を記憶 _NoFocusBackColor = MyBase.BackColor End Sub ''' <summary> ''' フォーカス時背景色 ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> <System.ComponentModel.DefaultValue(GetType(Color), "LemonChiffon")> _ Public Property FocusBackColor As Color = Color.LemonChiffon Protected Overrides Sub OnEnter(e As System.EventArgs) MyBase.OnEnter(e) 'フォーカス時背景色を設定 Me.BackColor = FocusBackColor End Sub Protected Overrides Sub OnLeave(e As System.EventArgs) MyBase.OnLeave(e) 'フォーカス無し背景色を設定 Me.BackColor = _NoFocusBackColor End Sub End Class
処理としては、Enter・Leave時にBackColorを設定し直しているだけで、特に難しい所はありません。デザイン画面で設定したBackColorは、InitLayout後にセットされるので、そこで一旦メンバ変数に退避しています。
さて、タイトルにある既定値の設定部分について説明します。
<System.ComponentModel.DefaultValue(GetType(Color), "LemonChiffon")> _ Public Property FocusBackColor As Color = Color.LemonChiffon
DefaultValueは、プロパティの既定値を設定しており、これを設定しておくと、
- プロパティが既定値の場合、不必要な初期値をソースコードに出力しなくなる
- デザイン画面でプロパティを右クリックしてリセット出来るようになる。
といった利点があります。ただし、このDefaultValueによる既定値は、定数しか設定出来ないという欠点があります。そのため、Color値を別クラスにして分けるといった事は出来ません。
既定値の設定方法はもう一つあり、それがShouldSerializeメソッド、Resetメソッドを作成する方法です。
ShouldSerialize メソッドと Reset メソッドによる既定値の定義
定数による単純な既定値を設定出来ない場合は、こちらを使用します。FocusBackColorの既定値を、ShouldSerializeメソッドで書き直した例を記述してみます。
Public Property FocusBackColor As Color = Color.LemonChiffon Private Function ShouldSerializeFocusBackColor() As Boolean 'Trueを返すとコードに保存する、Falseは保存しない(既定値) Return Me.FocusBackColor <> Color.LemonChiffon End Function Private Sub ResetFocusBackColor() Me.FocusBackColor = Color.LemonChiffon End Sub
プロパティ名の頭にShouldSerializeを付けたメソッドが既定値かどうかの判定処理、プロパティ名の頭にResetを付けたメソッドが、デザイン画面でリセットを実行した時の処理になります。
ShouldSerializeメソッドを作成する方法は、記述量は少し多くなりますが、比較対象が定数である必要がないので、より柔軟な判定が可能になります。ただし、DefaultValueとの併用は出来ないので注意してください。
Win7でProgressBarの表示が遅延する
Win7環境で(おそらくVistaも)、ビジュアルスタイルを有効にした状態で、一定速度以上でProgressBarのValue値をセットすると、セットしたValue値とProgressBarの表示が同期しません。
デモプログラムを用意してみました。
https://1drv.ms/u/s!AiiVDEkK6bacgViVOb2f2CrG3gwX
ProgressBarの最大値は100に設定してありますが、Win7で実行すると、Value値が100になってもデフォルトのProgressBarはゲージが満タンにならないのが確認できます。
遅延する詳細な原因は不明ですが、Win7のビジュアルスタイルのアニメーション効果に起因するものではないかと思います。ビジュアルスタイルを無効にした場合や、WinXPだと特に問題は発生しません。
デモプログラムのProgressBarExクラスでは、UserPaintを有効にして自力描画することにより、従来どおりValue値と表示を同期しています。しかしながら、ビジュアルスタイルのアニメーション効果は再現出来ないので、標準のProgressBarと同一の表示にはなりません。