none
Win 32 APIを使用して、メッセージボックスを親画面の中央に表示したい RRS feed

  • 質問

  • どなたかご教授お願いします。

    Win32 APIを使用して、メッセージボックスを親画面の中央に表示させたいのですが、実現できずにいます。

    C#ではサンプルとなるソースが色々とあったため、VB.NET にそのまま移植するつもりで実装しましたが、
    標準メッセージボックスの動きにしかなりません。

    環境: Windows 10 Professional
    開発環境: Visual Studio 2017

    ソースは以下の通りです。(長いため2つに分割します。)

    【C#】正常動作版
    -------------------------------------------------------
        public class CustomMessageBox
        {
            static class Win32Native
            {
                [DllImport("user32.dll")]
                static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
                const int GWL_HINSTANCE = -6;

                public static IntPtr GetWindowHInstance(IntPtr hWnd) => GetWindowLong(hWnd, GWL_HINSTANCE);

                [DllImport("kernel32.dll")]
                public static extern IntPtr GetCurrentThreadId();

                [DllImport("user32.dll")]
                static extern IntPtr SetWindowsHookEx(int idHook, HOOKPROC lpfn, IntPtr hInstance, IntPtr threadId);
                const int WH_CBT = 5;
                public static IntPtr SetWindowsHookEx(HOOKPROC lpfn, IntPtr hInstance, IntPtr threadId)
                                            => SetWindowsHookEx(WH_CBT, lpfn, hInstance, threadId);

                [DllImport("user32.dll")]
                public static extern bool UnhookWindowsHookEx(IntPtr hHook);
                [DllImport("user32.dll")]
                public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

                public delegate IntPtr HOOKPROC(int nCode, IntPtr wParam, IntPtr lParam);
                public const int HCBT_ACTIVATE = 5;

                [DllImport("user32.dll")]
                static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

                public struct RECT
                {
                    public int Left, Top, Right, Bottom;
                    public int Width => this.Right - this.Left;
                    public int Height => this.Bottom - this.Top;
                }

                public static RECT GetWindowRect(IntPtr hWnd)
                {
                    RECT rc;
                    GetWindowRect(hWnd, out rc);
                    return rc;
                }

                [DllImport("user32.dll")]
                static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter,
                    int x, int y, int cx, int cy, uint uFlags);
                const uint SWP_NOSIZE = 0x0001;
                const uint SWP_NOZORDER = 0x0004;
                const uint SWP_NOACTIVATE = 0x0010;

                public static bool SetWindowPos(IntPtr hWnd, int x, int y)
                {
                    var flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE;
                    return SetWindowPos(hWnd, 0, x, y, 0, 0, flags);
                }
            }

            private static IntPtr hOwner = (IntPtr)0;
            private static IntPtr hHook = (IntPtr)0;

            public static DialogResult Show(IWin32Window owner, string text, string caption,
                MessageBoxButtons buttons, MessageBoxIcon icon)
            {
                hOwner = owner.Handle;
                var hInstance = Win32Native.GetWindowHInstance(owner.Handle);
                var threadId = Win32Native.GetCurrentThreadId();

                hHook = Win32Native.SetWindowsHookEx(new Win32Native.HOOKPROC(HookProc), hInstance, threadId);
                return MessageBox.Show(owner, text, caption, buttons, icon);
            }

            private static IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode == Win32Native.HCBT_ACTIVATE)
                {
                    var rcOwner = Win32Native.GetWindowRect(hOwner);
                    var rcMsgBox = Win32Native.GetWindowRect(wParam);

                    var x = rcOwner.Left + (rcOwner.Width - rcMsgBox.Width) / 2;
                    var y = rcOwner.Top + (rcOwner.Height - rcMsgBox.Height) / 2;
                    Win32Native.SetWindowPos(wParam, x, y);

                    Win32Native.UnhookWindowsHookEx(hHook);
                    hHook = (IntPtr)0;
                }
                return Win32Native.CallNextHookEx(hHook, nCode, wParam, lParam);
            }
        }
    -------------------------------------------------------
    2019年6月30日 13:18

回答

  • すみません。ご質問のコードは検証できていませんが、インターネット上で同様の質問と回答が上がっておりました。英語になりますが、下記の投稿は参考になりますでしょうか。

    https://stackoverflow.com/questions/15904610/show-a-messagebox-centered-in-form

    • 回答としてマーク viator5 2019年6月30日 16:19
    2019年6月30日 14:06
  • 本題とは関係ないですが、VB 側で Module を使う必要性が感じられませんでした。

    static class を直訳しようとした結果とは思いますが、コードの意図を考えると、
    C# の Win32Native 実装のように、外部から触れないように設計した方が良いと思いますよ。

    Public NotInheritable Class CustomMessageBox
        Private NotInheritable Class Win32Native
            Private Declare Auto Function GetWindowLong Lib "user32" (hWnd As IntPtr, nindex As Integer) As IntPtr
            'Private Const GWL_HINSTANCE As Integer = -6
            'Public Shared ReadOnly GetWindowHInstance As Func(Of IntPtr, IntPtr) = Function(hWnd As IntPtr) GetWindowLong(hWnd, GWL_HINSTANCE)
            Public Shared Function GetWindowHInstance(hWnd As IntPtr) As IntPtr
                Const GWL_HINSTANCE As Integer = -6
                Return GetWindowLong(hWnd, GWL_HINSTANCE)
            End Function
            ' :
            ' :
        End Class
        ' :
        ' :
    End Class
    C#ではサンプルとなるソースが色々とあったため、VB.NET にそのまま移植するつもりで実装しましたが、
    標準メッセージボックスの動きにしかなりません。

    HCBT_ACTIVATE 時点の動作をステップ実行してみると、原因はすぐに分かるかと思いますよ。

    • 表示位置が変わらないのは、肝心の「SetWindowPos」を呼んでいないためです。
    • また、Show メソッド内で、Dim が 2 箇所余計に付与されているため、フィールド変数がローカル変数として再定義されてしまっています。
    • SetWindwPos に渡す変数 x と y が As Double 相当になっている点も問題です。C# の実装に合わせるなら、「/ 2」ではなく「\ 2」と訳すべきですし、こうした型指定のミスを防ぐためにも「Option Strict On」を指定しておくべきです。
    • Option Strict On にしておけば、HOOKPROC デリゲートの宣言ミスにも気づけたはず。
    • Property Width の実装ミスにより、常に固定値 0 が返却されてしまっています。 
    • ついでにいえば、「hHook = CType(0, IntPtr)」ではなく「hHook = IntPtr.Zero」の方がスマートです。

    • 編集済み 魔界の仮面弁士MVP 2019年6月30日 16:03 Width プロパティに関する指摘を追加
    • 回答としてマーク viator5 2019年6月30日 16:42
    • 回答としてマークされていない viator5 2019年6月30日 17:15
    • 回答としてマーク viator5 2019年6月30日 17:15
    2019年6月30日 15:59
  •     <DllImport("user32.dll")>
        Private Function GetWindowLong(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As IntPtr
        End Function

    64 ビットを考慮して GetWindowLongPtr を呼ぶようにしたほうが良いと思います。

    <DllImport("user32.dll", CharSet:=CharSet.Auto, EntryPoint:="GetWindowLong", ExactSpelling:=False)>
    Private Function GetWindowLong32(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As Integer
    End Function
    
    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=False)>
    Private Function GetWindowLongPtr(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As IntPtr
    End Function
    
    Private Function GetWindowLong(ByVal hwnd As System.IntPtr, ByVal nIndex As Integer) As IntPtr
        If Marshal.SizeOf(GetType(System.IntPtr)) <> Marshal.SizeOf(GetType(Integer)) Then
            Return GetWindowLongPtr(hwnd, nIndex)
        Else
            Return New IntPtr(GetWindowLong32(hwnd, nIndex))
        End If
    End Function
    


    • 編集済み KOZ6.0 2019年7月1日 2:16
    • 回答としてマーク viator5 2019年7月7日 5:30
    2019年7月1日 0:33
  •     <DllImport("user32.dll")>
        Private Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As Integer,
                                                 ByVal x As Integer, ByVal y As Integer,
                                                 ByVal cx As Integer, ByVal cy As Integer,
                                                 ByVal uFlags As UInteger) As Boolean
        End Function

    ここも、ByVal hWndInsertAfter As Integer ではなく ByVal hWndInsertAfter As IntPtr かと。

    • 回答としてマーク viator5 2019年7月7日 5:42
    2019年7月1日 0:51
  •     <DllImport("kernel32.dll")>
        Public Function GetCurrentThreadId() As IntPtr
        End Function

    Integer か UInteger ですね。

    もっともレジスタ渡しになっているので 64bit でも動きはするのですけど。(^_^;)

    • 回答としてマーク viator5 2019年7月7日 5:42
    2019年7月1日 1:05

すべての返信

  • 【VB.NET】中央に表示されません
    -------------------------------------------------------

    [Win32Nativeクラス]
    ---------
    Module Win32Native
        Private Const GWL_HINSTANCE As Integer = -6

        <DllImport("user32.dll")>
        Private Function GetWindowLong(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As IntPtr
        End Function

        Public Function GetWindowHInstance(ByVal hWnd As IntPtr) As IntPtr
            Return GetWindowLong(hWnd, GWL_HINSTANCE)
        End Function

        <DllImport("kernel32.dll")>
        Public Function GetCurrentThreadId() As IntPtr
        End Function

        Public Const WH_CBT As Integer = 5

        Public Const HCBT_ACTIVATE As Integer = 5

        <DllImport("user32.dll")>
        Private Function SetWindowsHookEx(ByVal idHock As Integer, ByVal lpfn As HOOKPROC,
                                          ByVal hInstance As IntPtr, ByVal threadId As IntPtr) As IntPtr
        End Function

        Public Function SetWindowsHookEx(ByVal lpfn As HOOKPROC, ByVal hInstance As IntPtr,
                                         ByVal threadId As IntPtr) As IntPtr
            Return SetWindowsHookEx(WH_CBT, lpfn, hInstance, threadId)
        End Function

        <DllImport("user32.dll")>
        Public Function UnhookWindowsHookEx(ByVal hHook As IntPtr) As Boolean
        End Function

        <DllImport("user32.dll")>
        Public Function CallNextHookEx(ByVal hHook As IntPtr, ByVal nCode As Integer,
                                                  ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
        End Function

        Public Delegate Function HOOKPROC(ByVal nCode As Integer, wParam As Integer, lParam As IntPtr) As IntPtr


        <DllImport("user32.dll")>
        Public Function GetWindowRect(ByVal hWnd As IntPtr, ByRef lpRect As RECT) As Boolean
        End Function

        Public Structure RECT
            Public Left, Top, Right, Bottom As Integer

            Public ReadOnly Property Width As Integer
                Get
                    Return Me.Right - Me.Right
                End Get
            End Property

            Public ReadOnly Property Height As Integer
                Get
                    Return Me.Bottom - Me.Top
                End Get
            End Property
        End Structure

        Public Function GetWindowRect(hWnd As IntPtr) As RECT
            Dim rc As RECT

            GetWindowRect(hWnd, rc)

            Return rc
        End Function

        <DllImport("user32.dll")>
        Private Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As Integer,
                                                 ByVal x As Integer, ByVal y As Integer,
                                                 ByVal cx As Integer, ByVal cy As Integer,
                                                 ByVal uFlags As UInteger) As Boolean
        End Function

        Private Const SWP_NOSIZE As UInteger = &H1 '0x0001;

        Private Const SWP_NOZORDER As UInteger = &H4 '0x0004;

        Private Const SWP_NOACTIVATE As UInteger = &H10 '0x0010;

        Private Function SetWindowPos(ByVal hWnd As IntPtr,
                                                 ByVal x As Integer, ByVal y As Integer) As Boolean
            Dim flags = SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOACTIVATE

            Return SetWindowPos(hWnd, 0, x, y, 0, 0, flags)
        End Function

    End Module
    ---------

    [CustomMessageBoxクラス]
    ---------
    Public Class CustomMessageBox

        ''' <summary>オーナハンドル</summary>
        Private Shared hOwner As IntPtr = IntPtr.Zero

        ''' <summary>フックハンドル</summary>
        Private Shared hHook As IntPtr = IntPtr.Zero

        Public Shared Function Show(ByVal owner As IWin32Window,
                                    ByVal text As String, ByVal caption As String,
                                    ByVal buttons As MessageBoxButtons, ByVal icon As MessageBoxIcon) As DialogResult
            Dim hOwner = owner.Handle
            Dim hInstance = Win32Native.GetWindowHInstance(owner.Handle)
            Dim threadId = Win32Native.GetCurrentThreadId()

            Dim hHook = Win32Native.SetWindowsHookEx(New Win32Native.HOOKPROC(AddressOf HookProc), hInstance, threadId)

            Return MessageBox.Show(owner, text, caption, buttons, icon)
        End Function

        Private Shared Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) _
                                            As IntPtr
            If nCode = Win32Native.HCBT_ACTIVATE Then
                ' オーナーウィンドウとメッセージボックスの領域を取得
                Dim rcOwner = Win32Native.GetWindowRect(hOwner)
                Dim rcMsgBox = Win32Native.GetWindowRect(wParam)

                ' メッセージボックスをオーナーウィンドウの中央位置に移動
                Dim x = rcOwner.Left + (rcOwner.Width - rcMsgBox.Width) / 2
                Dim y = rcOwner.Top + (rcOwner.Height - rcMsgBox.Height) / 2

                'フック解除
                Win32Native.UnhookWindowsHookEx(hHook)
                hHook = CType(0, IntPtr)
            End If

            Return Win32Native.CallNextHookEx(hHook, nCode, wParam, lParam)
        End Function

    End Class
    ---------
    -------------------------------------------------------
    2019年6月30日 13:19
  • すみません。ご質問のコードは検証できていませんが、インターネット上で同様の質問と回答が上がっておりました。英語になりますが、下記の投稿は参考になりますでしょうか。

    https://stackoverflow.com/questions/15904610/show-a-messagebox-centered-in-form

    • 回答としてマーク viator5 2019年6月30日 16:19
    2019年6月30日 14:06
  • 本題とは関係ないですが、VB 側で Module を使う必要性が感じられませんでした。

    static class を直訳しようとした結果とは思いますが、コードの意図を考えると、
    C# の Win32Native 実装のように、外部から触れないように設計した方が良いと思いますよ。

    Public NotInheritable Class CustomMessageBox
        Private NotInheritable Class Win32Native
            Private Declare Auto Function GetWindowLong Lib "user32" (hWnd As IntPtr, nindex As Integer) As IntPtr
            'Private Const GWL_HINSTANCE As Integer = -6
            'Public Shared ReadOnly GetWindowHInstance As Func(Of IntPtr, IntPtr) = Function(hWnd As IntPtr) GetWindowLong(hWnd, GWL_HINSTANCE)
            Public Shared Function GetWindowHInstance(hWnd As IntPtr) As IntPtr
                Const GWL_HINSTANCE As Integer = -6
                Return GetWindowLong(hWnd, GWL_HINSTANCE)
            End Function
            ' :
            ' :
        End Class
        ' :
        ' :
    End Class
    C#ではサンプルとなるソースが色々とあったため、VB.NET にそのまま移植するつもりで実装しましたが、
    標準メッセージボックスの動きにしかなりません。

    HCBT_ACTIVATE 時点の動作をステップ実行してみると、原因はすぐに分かるかと思いますよ。

    • 表示位置が変わらないのは、肝心の「SetWindowPos」を呼んでいないためです。
    • また、Show メソッド内で、Dim が 2 箇所余計に付与されているため、フィールド変数がローカル変数として再定義されてしまっています。
    • SetWindwPos に渡す変数 x と y が As Double 相当になっている点も問題です。C# の実装に合わせるなら、「/ 2」ではなく「\ 2」と訳すべきですし、こうした型指定のミスを防ぐためにも「Option Strict On」を指定しておくべきです。
    • Option Strict On にしておけば、HOOKPROC デリゲートの宣言ミスにも気づけたはず。
    • Property Width の実装ミスにより、常に固定値 0 が返却されてしまっています。 
    • ついでにいえば、「hHook = CType(0, IntPtr)」ではなく「hHook = IntPtr.Zero」の方がスマートです。

    • 編集済み 魔界の仮面弁士MVP 2019年6月30日 16:03 Width プロパティに関する指摘を追加
    • 回答としてマーク viator5 2019年6月30日 16:42
    • 回答としてマークされていない viator5 2019年6月30日 17:15
    • 回答としてマーク viator5 2019年6月30日 17:15
    2019年6月30日 15:59
  • kenjinote様

    早速のご返信ありがとうございます。

    ご紹介いただいたサイトで無事、意図する実装ができました。

    まだ、書きかけのソースとの違い、問題点まで確認できていませんが、
    明日、しっかり読んで理解したいと思います。

    英語のサイトまで検索の範囲を広げることが、すっかり抜けておりました。
    お恥ずかしいです。

    本当にありがとうございました。

    貴重なお時間いただいたこと、感謝いたします。
    2019年6月30日 16:23
  • 魔界の仮面弁士様

    ご返信、そして丁寧なソースチェック、ご指摘、誠にありがとうございます。
    (他サイトでも、度々、投稿された記事を参考にさせて頂いております。)


    > VB 側で Module を使う必要性が感じられませんでした。

    > static class を直訳しようとした結果とは思いますが、コードの意図を考えると、
    > C# の Win32Native 実装のように、外部から触れないように設計した方が良いと思いますよ。

    そうですね。
    画面の方から、使用する際、Moduleのメソッドが使用できる状態で、
    「良よろしくないクラス設計」という認識はあったのですが、
    「ともかく動くものを実装、それから修正」と考えておりました。

    的確なご指摘、とても有難いです。


    > 「Option Strict On」を指定しておくべきです。
    ありがとうございます。

    元々は必ず、指定する記述を行っていたのですが、最近LINQを使用する頻度が多くなり、
    すっかり宣言しないようになっていました。

    改めて、肝に銘じておきます。

    > 「hHook = CType(0, IntPtr)」ではなく「hHook = IntPtr.Zero」の方がスマートです。
    ここは上記の修正を行っていたのですが、古い記述で送信しておりました。



    まだ内容を十分、理解、ステップ実行もできていないのですが、
    本日は、とりあえず早々のご回答にお礼を申し上げたく、返信されていただきました。

    深夜にまで、貴重なお時間を頂き、ご返信ありがとうございました。

    追伸
    どうしても、理解できない箇所が出てきましたら、再度質問をさせてください。
    (ポイントをご教授いただけたので、原因を突き止められるとは思っているのですが。)
    2019年6月30日 17:17
  •     <DllImport("user32.dll")>
        Private Function GetWindowLong(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As IntPtr
        End Function

    64 ビットを考慮して GetWindowLongPtr を呼ぶようにしたほうが良いと思います。

    <DllImport("user32.dll", CharSet:=CharSet.Auto, EntryPoint:="GetWindowLong", ExactSpelling:=False)>
    Private Function GetWindowLong32(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As Integer
    End Function
    
    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=False)>
    Private Function GetWindowLongPtr(ByVal hWnd As IntPtr, ByVal nIndex As Integer) As IntPtr
    End Function
    
    Private Function GetWindowLong(ByVal hwnd As System.IntPtr, ByVal nIndex As Integer) As IntPtr
        If Marshal.SizeOf(GetType(System.IntPtr)) <> Marshal.SizeOf(GetType(Integer)) Then
            Return GetWindowLongPtr(hwnd, nIndex)
        Else
            Return New IntPtr(GetWindowLong32(hwnd, nIndex))
        End If
    End Function
    


    • 編集済み KOZ6.0 2019年7月1日 2:16
    • 回答としてマーク viator5 2019年7月7日 5:30
    2019年7月1日 0:33
  •     <DllImport("user32.dll")>
        Private Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As Integer,
                                                 ByVal x As Integer, ByVal y As Integer,
                                                 ByVal cx As Integer, ByVal cy As Integer,
                                                 ByVal uFlags As UInteger) As Boolean
        End Function

    ここも、ByVal hWndInsertAfter As Integer ではなく ByVal hWndInsertAfter As IntPtr かと。

    • 回答としてマーク viator5 2019年7月7日 5:42
    2019年7月1日 0:51
  •     <DllImport("kernel32.dll")>
        Public Function GetCurrentThreadId() As IntPtr
        End Function

    Integer か UInteger ですね。

    もっともレジスタ渡しになっているので 64bit でも動きはするのですけど。(^_^;)

    • 回答としてマーク viator5 2019年7月7日 5:42
    2019年7月1日 1:05
  • KOZ6.0様

    > 64 ビットを考慮して GetWindowLongPtr を呼ぶようにしたほうが良いと思います。

    ご指摘ありがとうございます。


    ご回答に気付かず、お礼の返信が遅くなり申し訳ありません。


    GetWindowLongPtrを始め、3連投のご指摘ありがとうございます。

    64bit への配慮、全く気づいておりませんでした。

    実装に反映いたします。
    2019年7月7日 5:42