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レイアウトや複雑な入力制御を伴うケースでは、後者のアーキテクチャを採用することが安定性を担保する上で現実的である。