トップ回答者
Win 32 APIを使用して、メッセージボックスを親画面の中央に表示したい

質問
-
どなたかご教授お願いします。
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);
}
}
-------------------------------------------------------
回答
-
本題とは関係ないですが、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
-
<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
-
<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
すべての返信
-
【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
---------
------------------------------------------------------- -
本題とは関係ないですが、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
-
魔界の仮面弁士様
ご返信、そして丁寧なソースチェック、ご指摘、誠にありがとうございます。
(他サイトでも、度々、投稿された記事を参考にさせて頂いております。)
> VB 側で Module を使う必要性が感じられませんでした。
> static class を直訳しようとした結果とは思いますが、コードの意図を考えると、
> C# の Win32Native 実装のように、外部から触れないように設計した方が良いと思いますよ。
そうですね。
画面の方から、使用する際、Moduleのメソッドが使用できる状態で、
「良よろしくないクラス設計」という認識はあったのですが、
「ともかく動くものを実装、それから修正」と考えておりました。
的確なご指摘、とても有難いです。
> 「Option Strict On」を指定しておくべきです。
ありがとうございます。
元々は必ず、指定する記述を行っていたのですが、最近LINQを使用する頻度が多くなり、
すっかり宣言しないようになっていました。
改めて、肝に銘じておきます。
> 「hHook = CType(0, IntPtr)」ではなく「hHook = IntPtr.Zero」の方がスマートです。
ここは上記の修正を行っていたのですが、古い記述で送信しておりました。
まだ内容を十分、理解、ステップ実行もできていないのですが、
本日は、とりあえず早々のご回答にお礼を申し上げたく、返信されていただきました。
深夜にまで、貴重なお時間を頂き、ご返信ありがとうございました。
追伸
どうしても、理解できない箇所が出てきましたら、再度質問をさせてください。
(ポイントをご教授いただけたので、原因を突き止められるとは思っているのですが。) -
<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
-
<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