Windows XPでフォーカス枠の太さをSystemParametersInfoで正しく変更する方法

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(項目によってはフラグなど)
pvParamOS が値を書き込む先のポインタ新しい値そのものを示す値(ポインタではない)
fWinIni通常 0SPIF_UPDATEINIFILE / SPIF_SENDCHANGE など

この「GET はポインタ、SET は値」というルールを忘れると、今回のような「6 桁の謎の数値」問題が発生します。

フォーカス枠関連の SPI 定数と役割

フォーカス枠に関係する SystemParametersInfo のアクションは次の 4 つです。

定数名値(16 進)方向内容
SPI_GETFOCUSBORDERWIDTH&H200EGETフォーカス枠の線の太さ(横方向)を取得
SPI_SETFOCUSBORDERWIDTH&H200FSETフォーカス枠の線の太さ(横方向)を設定
SPI_GETFOCUSBORDERHEIGHT&H2010GETフォーカス枠の線の太さ(縦方向)を取得
SPI_SETFOCUSBORDERHEIGHT&H2011SETフォーカス枠の線の太さ(縦方向)を設定

いずれも「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 時の pvParamIntPtr.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 向けのカスタマイズ機能を実装する際にも役立つ考え方なので、ぜひ覚えておいてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次