Windows XP でフォーカス枠(FocusRect)の太さを変更しようとして、SystemParametersInfo の戻り値が「6 桁の謎の数値」になって困ったことはないでしょうか。本記事では、その正体と VB.NET/Win32 API における正しい設定手順を、実例コード付きで丁寧に解説します。
Windows XP 32-bit で発生する「フォーカス枠の太さが変わらない」問題とは
Windows XP 32-bit 環境で、SystemParametersInfo API を使ってフォーカス枠(FocusRect)の太さを変更しようとすると、次のような現象に出会うことがあります。
SPI_SETFOCUSBORDERWIDTH/SPI_SETFOCUSBORDERHEIGHTで値を 4 に設定した。- 直後に
SPI_GETFOCUSBORDERWIDTH/SPI_GETFOCUSBORDERHEIGHTで読み出すと、「6 桁くらいの謎の大きな数」 が返ってくる。 - しかも、フォーカス枠が実際に太くなっているようにも見えない。
この状況から、次のような疑問が出てきます。
pvParamにビットマスクを渡す仕様なのか?- どこかのレジストリ値を書き換える必要があるのか?
- XP 固有のバグなのか?
結論から言うと、ビットマスクやレジストリ操作は関係ありません。原因は、SystemParametersInfo の第 3 引数 pvParam への「値の渡し方」が誤っていることです。
SystemParametersInfo の基本仕様をおさらい
まずは、SystemParametersInfo のパラメータの役割を整理しておきましょう。宣言(VB の例)は以下のようになります。
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function SystemParametersInfo(
ByVal uiAction As UInteger,
ByVal uiParam As UInteger,
ByVal pvParam As IntPtr,
ByVal fWinIni As UInteger
) As Boolean
End Function
各引数の意味を、GET 系とSET 系に分けて表にすると分かりやすくなります。
| 役割 | GET 系(SPI_GETXXXX) | SET 系(SPI_SETXXXX) |
|---|---|---|
uiAction | 取得したいパラメータを指定 | 設定したいパラメータを指定 |
uiParam | 多くの場合 0(項目によってはバッファサイズなど) | 多くの場合 0(項目によってはフラグなど) |
pvParam | OS が値を書き込む先のポインタ | 新しい値そのものを示す値(ポインタではない) |
fWinIni | 通常 0 | SPIF_UPDATEINIFILE / SPIF_SENDCHANGE など |
この「GET はポインタ、SET は値」というルールを忘れると、今回のような「6 桁の謎の数値」問題が発生します。
フォーカス枠関連の SPI 定数と役割
フォーカス枠に関係する SystemParametersInfo のアクションは次の 4 つです。
| 定数名 | 値(16 進) | 方向 | 内容 |
|---|---|---|---|
SPI_GETFOCUSBORDERWIDTH | &H200E | GET | フォーカス枠の線の太さ(横方向)を取得 |
SPI_SETFOCUSBORDERWIDTH | &H200F | SET | フォーカス枠の線の太さ(横方向)を設定 |
SPI_GETFOCUSBORDERHEIGHT | &H2010 | GET | フォーカス枠の線の太さ(縦方向)を取得 |
SPI_SETFOCUSBORDERHEIGHT | &H2011 | SET | フォーカス枠の線の太さ(縦方向)を設定 |
いずれも「1 単位の整数値」で、ビットマスクではありません。pvParam にもビットマスクを渡しません。
よくある間違い:SET 時にもポインタを渡してしまう
問題の原因は、フォーカス枠の GET と SET の書き方をコピペで統一してしまうことです。ありがちな誤った VB.NET のコード例を見てみましょう。
' ★間違いパターン(イメージ)★
Dim buf As IntPtr = Marshal.AllocCoTaskMem(4)
' 現在値を取得(これは OK)
SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH, 0UI, buf, 0UI)
Dim currentWidth As Integer = Marshal.ReadInt32(buf)
' 新しい値 4 をセット(NG:ポインタをそのまま渡している)
Marshal.WriteInt32(buf, 4)
SystemParametersInfo(SPI_SETFOCUSBORDERWIDTH, 0UI, buf, 0UI)
' 再取得すると…
SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH, 0UI, buf, 0UI)
Dim width As Integer = Marshal.ReadInt32(buf) ' ここで 123456 みたいな値になる
このコードでは、SET 時にも pvParam に 「値を格納したバッファのポインタ」を渡しています。しかし SPI_SETFOCUSBORDERWIDTH の仕様は、pvParam に「新しい太さそのもの」を渡すことになっています。
OS 側は pvParam を「整数値」として読み取るため、ポインタの数値(たとえば &H00A1B2C3)をそのまま太さとして扱ってしまうのです。その結果、次のようになります。
- 設定された太さは、実際には巨大な値(ポインタの値)。
- 再取得すると、その巨大な値がそのまま返される。
- 画面描画側がその値を適切に解釈できず、見た目が変わらない場合もある。
つまり、「6 桁の謎の値」 の正体は ポインタ値です。ビットマスクでも XP のバグでもありません。
正しい呼び出し方:GET はポインタ、SET は値そのもの
では、正しいコードはどう書くべきでしょうか。大切なのは、次の 2 点です。
- GET(SPI_GETXXXX):
pvParamに受け取り先バッファのポインタを渡す。 - SET(SPI_SETXXXX):
pvParamに新しい値(整数)をIntPtrとして渡す。
VB.NET による最小サンプルは次の通りです。
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function SystemParametersInfo(
ByVal uiAction As UInteger,
ByVal uiParam As UInteger,
ByVal pvParam As IntPtr,
ByVal fWinIni As UInteger
) As Boolean
End Function
Private Const SPI_GETFOCUSBORDERWIDTH As UInteger = &H200EUI
Private Const SPI_SETFOCUSBORDERWIDTH As UInteger = &H200FUI
Private Const SPI_GETFOCUSBORDERHEIGHT As UInteger = &H2010UI
Private Const SPI_SETFOCUSBORDERHEIGHT As UInteger = &H2011UI
Private Const SPIF_UPDATEINIFILE As UInteger = &H1UI
Private Const SPIF_SENDCHANGE As UInteger = &H2UI
' フォーカス枠の幅を取得
Private Function GetFocusBorderWidth() As Integer
Dim buf As IntPtr = Marshal.AllocCoTaskMem(4) ' Int32 用
Try
Dim ok As Boolean = SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH, 0UI, buf, 0UI)
If Not ok Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
Return Marshal.ReadInt32(buf)
Finally
Marshal.FreeCoTaskMem(buf)
End Try
End Function
' フォーカス枠の幅を設定
Private Sub SetFocusBorderWidth(newWidth As Integer)
Dim ok As Boolean = SystemParametersInfo(
SPI_SETFOCUSBORDERWIDTH,
0UI,
CType(newWidth, IntPtr),
SPIF_UPDATEINIFILE Or SPIF_SENDCHANGE)
If Not ok Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
End Sub
ポイントは、SET の時に
CType(newWidth, IntPtr)で 整数値をIntPtrに変換して渡していること。SPIF_UPDATEINIFILE Or SPIF_SENDCHANGEによって 設定を永続化し、他プロセスに通知していること。
GET と SET の pvParam の使い方が、正反対であることに注意してください。
幅と高さをまとめて扱うヘルパークラスの例
アプリケーションの中では、幅と高さをセットで扱うことが多いので、ラッパークラスを用意してしまうと便利です。
Public Class FocusBorderHelper
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function SystemParametersInfo(
ByVal uiAction As UInteger,
ByVal uiParam As UInteger,
ByVal pvParam As IntPtr,
ByVal fWinIni As UInteger
) As Boolean
End Function
Private Const SPI_GETFOCUSBORDERWIDTH As UInteger = &H200EUI
Private Const SPI_SETFOCUSBORDERWIDTH As UInteger = &H200FUI
Private Const SPI_GETFOCUSBORDERHEIGHT As UInteger = &H2010UI
Private Const SPI_SETFOCUSBORDERHEIGHT As UInteger = &H2011UI
Private Const SPIF_UPDATEINIFILE As UInteger = &H1UI
Private Const SPIF_SENDCHANGE As UInteger = &H2UI
Public Shared Function GetFocusBorderWidth() As Integer
Return GetIntValue(SPI_GETFOCUSBORDERWIDTH)
End Function
Public Shared Function GetFocusBorderHeight() As Integer
Return GetIntValue(SPI_GETFOCUSBORDERHEIGHT)
End Function
Public Shared Sub SetFocusBorderSize(width As Integer, height As Integer)
SetIntValue(SPI_SETFOCUSBORDERWIDTH, width)
SetIntValue(SPI_SETFOCUSBORDERHEIGHT, height)
End Sub
Private Shared Function GetIntValue(action As UInteger) As Integer
Dim buf As IntPtr = Marshal.AllocCoTaskMem(4)
Try
Dim ok As Boolean = SystemParametersInfo(action, 0UI, buf, 0UI)
If Not ok Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
Return Marshal.ReadInt32(buf)
Finally
Marshal.FreeCoTaskMem(buf)
End Try
End Function
Private Shared Sub SetIntValue(action As UInteger, value As Integer)
Dim ok As Boolean = SystemParametersInfo(
action,
0UI,
CType(value, IntPtr),
SPIF_UPDATEINIFILE Or SPIF_SENDCHANGE)
If Not ok Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
End Sub
End Class
これで、呼び出し側は次のようにシンプルになります。
' 現在の太さを確認
Dim w As Integer = FocusBorderHelper.GetFocusBorderWidth()
Dim h As Integer = FocusBorderHelper.GetFocusBorderHeight()
' 太さを 4 に変更
FocusBorderHelper.SetFocusBorderSize(4, 4)
動作確認のコツ:DrawFocusRect で目視チェック
フォーカス枠の太さは、実際のコントロールやテーマによっては見分けにくいことがあります。その場合、DrawFocusRect API を使って、テスト用の長方形を描画してみると分かりやすくなります。
VB.NET(Windows フォーム)でのシンプルな例を示します。
<DllImport("user32.dll")>
Private Shared Sub DrawFocusRect(hdc As IntPtr, ByRef lprc As RECT)
End Sub
<StructLayout(LayoutKind.Sequential)>
Private Structure RECT
Public Left As Integer
Public Top As Integer
Public Right As Integer
Public Bottom As Integer
End Structure
Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles MyBase.Paint
Dim rect As New RECT() With {
.Left = 20,
.Top = 20,
.Right = 200,
.Bottom = 80
}
Dim hdc As IntPtr = e.Graphics.GetHdc()
Try
DrawFocusRect(hdc, rect)
Finally
e.Graphics.ReleaseHdc(hdc)
End Try
End Sub
この状態で SetFocusBorderSize(1, 1) と SetFocusBorderSize(4, 4) を切り替えてみると、環境によっては線の太さが変化して見えるはずです。
エラー処理とトラブルシューティング
SystemParametersInfo は、失敗すると FALSE を返し、GetLastError に詳細なエラーコードを残します。P/Invoke では、次のように例外へ変換しておくとデバッグが楽になります。
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function SystemParametersInfo(
ByVal uiAction As UInteger,
ByVal uiParam As UInteger,
ByVal pvParam As IntPtr,
ByVal fWinIni As UInteger
) As Boolean
End Function
Private Sub SafeSetFocusBorderWidth(newWidth As Integer)
Dim ok As Boolean = SystemParametersInfo(
SPI_SETFOCUSBORDERWIDTH,
0UI,
CType(newWidth, IntPtr),
SPIF_UPDATEINIFILE Or SPIF_SENDCHANGE)
If Not ok Then
Dim err As Integer = Marshal.GetLastWin32Error()
Throw New Win32Exception(err)
End If
End Sub
チェックしておきたいポイントを表にまとめます。
| チェック項目 | 確認内容 |
|---|---|
SET 時の pvParam | ポインタではなく、CType(newWidth, IntPtr) のように「値」を渡しているか。 |
GET 時の pvParam | IntPtr.Zero のまま呼んでいないか。4 バイト以上のメモリを確保しているか。 |
| 戻り値 | SystemParametersInfo の戻り値をチェックし、FALSE なら GetLastWin32Error() を調べているか。 |
fWinIni フラグ | 他プロセスにも変更を伝えたい場合は SPIF_SENDCHANGE を付けているか。 |
| 実描画の確認 | DrawFocusRect や自作コントロールで線の太さを目視確認しているか。 |
XP / テーマ依存の注意点
ここまでは「API の呼び出し方」の話でしたが、実際の見た目(UI)に関しては Windows XP ならではの注意点があります。
- Windows XP はすでにサポート終了しており、テーマやビジュアルスタイルによっては フォーカス枠の太さ設定が UI に素直に反映されない場合があります。
- 一部の標準コントロールは、内部で固定値の太さを使って描画しており、 システム設定を参照しないこともあります。
- 同じコードでも Windows 7 / Windows 10 / Windows 11 では挙動が微妙に異なることがあります。
重要なのは、
- SystemParametersInfo の仕様(GET はポインタ、SET は値)は OS バージョンに関係なく同じであること。
- 見た目が変わるかどうかは、「その時のテーマ」と「各コントロールの実装」に依存すること。
フォーカス枠の太さがどうしても視認性に影響するアプリケーションでは、自前描画でフォーカス枠を描く(ControlPaint.DrawFocusRectangle や GDI を使う)ことも検討するとよいでしょう。
レジストリを直接いじる必要はある?
SystemParametersInfo の中には、内部的にレジストリを書き換えるものもありますが、「どのキーに保存しているか」は公式には公開されていません。フォーカス枠の太さも同様です。
そのため、
- レジストリを直接編集して挙動を変えるのは非推奨。
- 設定・取得は 必ず
SystemParametersInfo経由で行う。 - アプリ側で以前の値を戻したい場合は、変更前に
SPI_GETFOCUSBORDERWIDTH/HEIGHTで退避しておく。
レジストリの場所が分からないことを逆に利用し、「API ラッパーでのみ変更する」というルールにしておくと、安全に運用できます。
32-bit / 64-bit の違いと IntPtr の使い方
この記事では Windows XP 32-bit を前提にしていますが、将来的に 64-bit 環境でも同じコードを使いたい場合は、IntPtr を正しく使うことが重要になります。
- 32-bit 環境では
IntPtrのサイズは 4 バイト。 - 64-bit 環境では
IntPtrのサイズは 8 バイト。
次のような NG パターンは避けましょう。
' NG: IntPtr を Integer にキャストして保存してしまう
Dim value As Integer = CType(ptr, Integer)
64-bit 環境では、ポインタの上位 4 バイトが失われます。ポインタはあくまで IntPtr 型で保持し、整数値として扱いたいときだけ CType(値, IntPtr) として明示的に変換するのが安全です。
C# 版のサンプル(参考)
VB.NET から C# へ移植する場合の参考として、C# 版の最小サンプルも載せておきます。
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool SystemParametersInfo(
uint uiAction,
uint uiParam,
IntPtr pvParam,
uint fWinIni);
private const uint SPI_GETFOCUSBORDERWIDTH = 0x0000200E;
private const uint SPI_SETFOCUSBORDERWIDTH = 0x0000200F;
private const uint SPI_GETFOCUSBORDERHEIGHT = 0x00002010;
private const uint SPI_SETFOCUSBORDERHEIGHT = 0x00002011;
private const uint SPIF_UPDATEINIFILE = 0x0001;
private const uint SPIF_SENDCHANGE = 0x0002;
private static int GetFocusBorderWidth()
{
IntPtr buf = Marshal.AllocCoTaskMem(4);
try
{
if (!SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH, 0u, buf, 0u))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return Marshal.ReadInt32(buf);
}
finally
{
Marshal.FreeCoTaskMem(buf);
}
}
private static void SetFocusBorderWidth(int width)
{
if (!SystemParametersInfo(
SPI_SETFOCUSBORDERWIDTH,
0u,
new IntPtr(width),
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
VB.NET と同様に、
- GET では
pvParamにバッファのポインタ(IntPtr)。 - SET では
pvParamにnew IntPtr(width)で値そのもの。
というルールだけは崩さないようにしてください。
よくある疑問とまとめ
Q. どうしても太さが変わっているように見えません。
A. XP のテーマや描画スタイルによっては、システム設定を参照しないコントロールがあります。その場合は、自前でフォーカス描画を行う(たとえば OnPaint で ControlPaint.DrawFocusRectangle を使う)ことを検討してください。
Q. 6 桁の値になってもエラーが出ないのはなぜ?
A. API としては「整数値を受け取り、対応する内部状態に設定する」ことに成功しているためです。たとえそれがポインタ値であっても、仕様上はエラーではありません。値の意味を間違えないようにするのはアプリ側の責任です。
Q. Windows 7 / 10 でも同じコードを使って大丈夫?
A. SystemParametersInfo の基本仕様は変わりません。ただし、フォーカス枠の描画有無や見た目は OS + テーマ次第です。XP 用に書いたコードがそのまま同じ見た目になるとは限らない点に注意してください。
Q. なぜビットマスクだと思ってしまいがちなの?
A. 「謎の大きな数値」が返ってくると、人はつい「フラグの組み合わせ」だと考えがちです。しかし今回のケースでは、単純に「ポインタを整数値として扱ってしまった」だけです。仕様書を読み直し、「GET はポインタ、SET は値」を徹底すれば、この誤解は防げます。
この記事のまとめ
最後に、本記事の要点を整理します。
- SystemParametersInfo + SPI_SETFOCUSBORDERWIDTH/HEIGHT で 6 桁の謎の数値が返る原因は、SET 時にポインタを渡しているため。
- フォーカス枠の太さは ビットマスクではなく、単純な整数値。
- GET:
pvParamに受け取り用バッファのポインタを渡す。 - SET:
pvParamに新しい値そのものをIntPtrとして渡す。 - 変更を他プロセスに反映させるには
SPIF_UPDATEINIFILE Or SPIF_SENDCHANGEを指定する。 - 見た目に反映されるかどうかは、XP のテーマやコントロール実装に依存する。
- レジストリ直接編集は避け、API ラッパー経由で取得・設定するのが安全。
以上のポイントを押さえておけば、「値を 4 に設定したのに読み出しで巨大な数になる」「フォーカス枠が太くなったりならなかったりする」といったハマりどころを、落ち着いて切り分けられるようになります。Windows XP でのレガシー保守はもちろん、Windows 10 / 11 向けのカスタマイズ機能を実装する際にも役立つ考え方なので、ぜひ覚えておいてください。

コメント