radianの備忘録

プログラミングやPCに関する事をメモ代わりに残していきます

独自メッセージボックス

特に更新するネタも無かったので放置してました。
相変わらずWinFormプログラムばかり書いています。

Windows標準のメッセージボックスはOS標準のフォントサイズで表示されるのですが、最近はタブレットやタッチパネル用途のプログラムを作る事もあり、文字が小さくて読みにくかったり、指で操作するにはボタンが若干小さいように感じられました。

メッセージボックスを使わないというのも一つの選択肢ですが、それでもまだ使いどころはあると思うので、自前でカスタマイズ可能なメッセージボックスを作成してみました。

f:id:radian_jp:20181008154544p:plain
標準のメッセージボックス(Win10)

f:id:radian_jp:20181008154552p:plain
独自メッセージボックス(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です。

f:id:radian_jp:20141118144914p:plain

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

半透明なコントロールを作成する

通常、コントロールは透過色をサポートしていませんが、コントロール作成時にいくつか設定を行うことで透過色を使うことが出来るようになります。

f:id:radian_jp:20141112171825j:plain

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を設定したコントロール同士が重なった場合、どちらが先に描画されるか判らなくなるため、描画順がおかしくなります。

f:id:radian_jp:20141113140331j:plain

この画像の例では、デザイナ画面では黄色のラベルを最前面に設定しているのですが、青色のラベルが黄色の手前に表示されているのが判ると思います。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の表示が同期しません。

 

f:id:radian_jp:20141016144057p:plain

 

デモプログラムを用意してみました。

https://1drv.ms/u/s!AiiVDEkK6bacgViVOb2f2CrG3gwX

 

ProgressBarの最大値は100に設定してありますが、Win7で実行すると、Value値が100になってもデフォルトのProgressBarはゲージが満タンにならないのが確認できます。

 

遅延する詳細な原因は不明ですが、Win7のビジュアルスタイルのアニメーション効果に起因するものではないかと思います。ビジュアルスタイルを無効にした場合や、WinXPだと特に問題は発生しません。

 

デモプログラムのProgressBarExクラスでは、UserPaintを有効にして自力描画することにより、従来どおりValue値と表示を同期しています。しかしながら、ビジュアルスタイルのアニメーション効果は再現出来ないので、標準のProgressBarと同一の表示にはなりません。