WinFormsにおける特定コントロールのフルスクリーン化手法

WinFormsアプリケーションの開発において、動画再生領域や地図表示ウィジェットなどの特定のUIコンポーネントのみを画面全体に拡張したい要件が生じる場合がある。このようなケースでは、メインフォームのレイアウトを維持しつつ対象コントロールだけを切り離す実装が必要となる。本稿では、その実現アプローチとして確立された2つのパターンを解説する。

1. Win32 API(SetParent)による親ウィンドウの再アサイン

この手法は、対象コントロールの親ウィンドウハンドルをWin32 APIの SetParent を介してデスクトップ(親なし状態)へ変更する。これにより、コントロールはフォームの階層構造から独立し、画面全体に配置可能になる。

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

internal class ApiFullscreenManager
{
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

    private Control _target;
    private IntPtr _prevParentHandle;
    private DockStyle _prevDock;

    public void EnterFullscreen(Control ctrl)
    {
        _target = ctrl;
        _prevParentHandle = ctrl.Parent?.Handle ?? IntPtr.Zero;
        _prevDock = ctrl.Dock;

        ctrl.Dock = DockStyle.None;
        ctrl.Bounds = Screen.PrimaryScreen.Bounds;
        AttachEscapeKey(ctrl, ExitFullscreen);
        ctrl.Focus();
        SetParent(ctrl.Handle, IntPtr.Zero);
    }

    public void ExitFullscreen(Control ctrl)
    {
        if (_target == null || _prevParentHandle == IntPtr.Zero) return;

        SetParent(_target.Handle, _prevParentHandle);
        _target.Dock = _prevDock;
        _target = null;
    }

    private void AttachEscapeKey(Control node, Action<Control> callback)
    {
        if (node == null) return;
        node.KeyUp += (s, e) => { if (e.KeyCode == Keys.Escape) callback(node); };
        foreach (Control child in node.Controls)
            AttachEscapeKey(child, callback);
    }
}

上記実装では、フルスクリーン移行時にコントロールの元の親ハンドルと Dock 設定を保持し、EnterFullscreen 呼び出し後に座標を画面解像度に合わせる。キーボード入力を受け付けるため、明示的に Focus() を発行している。ESCキーが押下された際、再帰的に子コントロールまでイベントを紐付け、元の階層に復帰させる処理が走る。

ただし、この手法には若干の制約がある。コントロール上にコンテキストメニューやサブウィンドウが存在する場合、親ウィンドウの切り替えによりメッセージループがメインフォームに流れてしまい、マウス操作の応答性やメニュー表示位置に齟齬が生じることがある。

2. 専用コンテナフォームを利用した切り離し

より安定した動作を期待する場合は、フルスクリーン表示専用の中継フォームを動的に生成し、対象コントロールをそこに移管するアプローチが有効である。この方法では、Windowsメッセージループの階層を明確に分けることができる。

using System;
using System.Windows.Forms;

internal class FormBasedFullscreenManager
{
    private FullScreenHost _hostForm;
    private Control _target;
    private Control _originalParent;

    public void EnterFullscreen(Control ctrl, Control parent)
    {
        _target = ctrl;
        _originalParent = parent;

        parent.Controls.Remove(ctrl);
        _hostForm = new FullScreenHost(ctrl);
        AttachEscapeKey(ctrl, ExitFullscreen);
        _hostForm.ShowDialog();
    }

    private void ExitFullscreen(Control ctrl)
    {
        if (_hostForm != null && !_hostForm.IsDisposed)
        {
            _hostForm.Controls.Remove(ctrl);
            _originalParent?.Controls.Add(ctrl);
            _hostForm.Close();
        }
    }

    private void AttachEscapeKey(Control node, Action<Control> callback)
    {
        if (node == null) return;
        node.KeyUp += (s, e) => { if (e.KeyCode == Keys.Escape) callback(node); };
        foreach (Control child in node.Controls)
            AttachEscapeKey(child, callback);
    }
}

internal class FullScreenHost : Form
{
    public FullScreenHost(Control payload)
    {
        FormBorderStyle = FormBorderStyle.None;
        WindowState = FormWindowState.Maximized;
        TopMost = true;
        Controls.Add(payload);
        payload.Dock = DockStyle.Fill;
    }
}

このパターンでは、FullScreenHost クラスが境界線なし・最大化・常時最前面のウィンドウを提供する。対象コントロールは元の親から一度削除され、コンテナフォーム内に DockStyle.Fill で再配置される。ESCキー発火時には、コントロールを元の親コレクションに戻した直後に Close() を実行する順序が重要であり、逆転するとリソース解放のタイミングで例外を投げる可能性がある。

実環境での検証結果を踏まえると、Win32 APIを直接操作する方法は軽量だが、UIイベントのバブリングやフォーカス管理に手動の補正を要する傾向がある。一方、中継フォームを利用する手法は、Windowsフォームエンジンが提供する標準的なメッセージ処理をそのまま活用できるため、コンテキストメニューやツールチップ、マウスホイールイベントなどが正常に動作する。大規模なUIレイアウトや複雑な入力制御を伴うケースでは、後者のアーキテクチャを採用することが安定性を担保する上で現実的である。

タグ: C# WinForms Win32API SetParent UI制御

5月29日 13:57 投稿