.NETにおけるコンポーネント設計とIExtenderProviderを活用したUI制御の拡張手法

コンポーネントの定義とクラス・コントロールの関係性

プログラミング領域における「コンポーネント」という用語は文脈により曖昧だが、.NETフレームワークでは厳密に規定されている。具体的には`System.ComponentModel.IComponent`インターフェースを実装したクラス、あるいは該インタフェースを間接的に実装する基底クラスを継承したクラスを指す。WinFormsやWebFormsにおけるコントロールも内部設計上`System.Windows.Forms.Control`から派生しており、その親階層として`System.ComponentModel.Component`が存在する。したがって、「すべてのコントロールはコンポーネントの一種である」という包含関係が成立する。

  • 概念的な階層:クラス > コンポーネント > コントロール
  • クラスのインスタンスはすべてコンポーネントとは限らないが、コンポーネントは常にクラスであり、コントロールはコンポーネントの具現化形態の一つである。
  • それぞれオブジェクト指向の再頒布可能単位として設計されるが、UIレンダリングの有無とIDE上の扱いが区別点となる。

FCLの実装例としては、`Application`クラスは純粋なユーティリティクラスでありコンポーネントではない。対照的に`Timer`や`ImageList`は非ビジュアルコンポーネントであり、`Button`はビジュアルコントロールでありながらコンポーネントの特性も併せ持つ。

コンポーネントの核心特性

  1. リソースLifecycleの標準化: `IComponent`は`IDisposable`を継承している。これにより、アンマネージドリソースや外部ファイルハンドル、ストリームなどの割り当てと解放を統一的なDisposeパターンで扱える設計が強制される。
  2. デザインタイム統合: Visual Studioなどの開発環境において、`IComponent`を実装すればツールボックスへのドラッグ&ドロップやプロパティグリッドでの編集が自動的に有効化される。
  3. コンテナによる論理カプセル化: フォームデザイナが出力する`InitializeComponent()`メソッド内を見ると、コンポーネントは`System.ComponentModel.IContainer`型のメンバ経由で初期化される。これは通常の配列やリストによる要素保持とは異なり、コンテナ自体のDisposeタイミングに連動して内部的な所有リソースを一括開放する論理的所有権モデルである。

IExtenderProviderによる動的プロパティ注入

既存コントロールの機能をカスタマイズする場合、直交する継承階層を作ることは保守性を低下させる。特定の属性(例:ツールチップ、エラーアイコン、視覚フィルタ)を多数のコントロールに追加したい際は、`System.ComponentModel.IExtenderProvider`パターンが適している。このインタフェースは単一のメソッド`CanExtend(object target)`のみを要求し、対象型に対する拡張許可をブーリアンで返却する。このパターンを利用すると、独自のコンポーネントをフォーム上に配置するだけで、他のコントロールのプロパティエディタに新規項目として反映させることができる。

標準ライブラリの`ToolTip`コンポーネントがこの設計を採用しており、`[ProvideProperty("ToolTip", typeof(Control))]`属性と組み合わせて、ボタンやラベルなどの既存UIに対して動的にツールテキストを紐づける機構を実現している。

実践実装:コントロール鏡像表示プロバイダ

IExtenderProviderの典型例として、任意のコントロールに反射(ミラー)描写を付与するコンポーネントを作成する。この場合、各コントロールに`EnableReflection`という論理プロパティを追加し、有効時に対応する描画オーバーレイが自動的に生成・配置されるアーキテクチャとなる。

拡張プロバイダ実装

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace UIExtensions
{
    [ProvideProperty("EnableReflection", typeof(Control))]
    public class ReflectionProvider : Component, IExtenderProvider
    {
        private readonly Dictionary<Control, ReflectionSurface> _activeSurfaces = new();

        public ReflectionProvider() { }
        public ReflectionProvider(IContainer container) => container.Add(this);

        public bool CanExtend(object target) => target is Control && !(target is ReflectionOverlay);

        [Category("Appearance")]
        [Description("Enable visual mirror effect for the target control")]
        public void SetEnableReflection(Control target, bool value)
        {
            if (_activeSurfaces.TryGetValue(target, out var surface))
            {
                if (!value)
                {
                    surface.Dispose();
                    _activeSurfaces.Remove(target);
                }
            }
            else if (value)
            {
                var overlay = new ReflectionOverlay(target);
                _activeSurfaces[target] = new ReflectionSurface(overlay);
            }
        }

        [Category("Appearance")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool GetEnableReflection(Control target)
        {
            return _activeSurfaces.ContainsKey(target);
        }

        private sealed class ReflectionSurface : IDisposable
        {
            private readonly ReflectionOverlay _overlay;
            public ReflectionSurface(ReflectionOverlay overlay) => _overlay = overlay;
            public void Dispose() => _overlay.Dispose();
        }
    }
}

鏡像描画レイヤークラス

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

namespace UIExtensions
{
    public class ReflectionOverlay : Control
    {
        private readonly Control _originalTarget;
        private Padding _paddingOffset = new(0, 0, 0, 20);
        private byte[] _cachedPixelData;
        private int _strideLength;
        private Bitmap _sourceSnapshot;
        private bool _isCapturing = false;

        public ReflectionOverlay(Control target)
        {
            _originalTarget = target;
            _originalTarget.VisibleChanged += OnTargetStateChanged;
            _originalTarget.LocationChanged += OnTargetMoved;
            _originalTarget.ParentChanged += OnParentChanged;
            _originalTarget.Paint += OnTargetPainted;

            SetStyle(ControlStyles.Selectable | ControlStyles.UserPaint |
                     ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
            ResizeRedraw = true;
        }

        private void OnTargetStateChanged(object s, EventArgs e) => RecalculateLayout();
        private void OnTargetMoved(object s, EventArgs e) => RecalculateLayout();
        private void OnParentChanged(object s, EventArgs e) => InitializeOverlay();
        private void OnTargetPainted(object sender, PaintEventArgs e) { if (!_isCapturing) Invalidate(); }

        private void RecalculateLayout()
        {
            if (Parent == null) return;
            Size requiredSize = new(_originalTarget.Width + _paddingOffset.Horizontal, _originalTarget.Height + _paddingOffset.Vertical);
            if (Size != requiredSize) Size = requiredSize;
            
            Location = _originalTarget.Location;
            Visible = _originalTarget.Visible;
            Parent.Controls.SetChildIndex(this, Parent.Controls.GetChildIndex(_originalTarget) + 1);
        }

        private void InitializeOverlay()
        {
            Parent ??= _originalTarget.Parent;
            RecalculateLayout();
        }

        private Bitmap CaptureForeground()
        {
            if (_originalTarget.IsDisposed) return null;
            _isCapturing = true;
            var bitmap = new Bitmap(ClientRectangle.Width, ClientRectangle.Height);
            _originalTarget.DrawToBitmap(bitmap, new Rectangle(Padding.Left, Padding.Top, _originalTarget.Width, _originalTarget.Height));
            _isCapturing = false;
            return bitmap;
        }

        private static byte[] ExtractPixels(Bitmap image)
        {
            const int BytePerPixel = 4;
            var boundsRect = new Rectangle(0, 0, image.Width, image.Height);
            var data = image.LockBits(boundsRect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
            var buffer = new byte[image.Width * image.Height * BytePerPixel];
            Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
            image.UnlockBits(data);
            return buffer;
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            base.OnPaint(pe);
            try
            {
                _sourceSnapshot?.Dispose();
                _sourceSnapshot = CaptureForeground();
                if (_sourceSnapshot == null) return;

                _cachedPixelData = ExtractPixels(_sourceSnapshot);
                _strideLength = _sourceSnapshot.Width * 4;

                using var resultCanvas = new Bitmap(Width, Height);
                var pixels = ExtractPixels(resultCanvas);
                
                ApplyVerticalMirror(pixels);
                
                // Re-lock to write back processed pixels
                var rBounds = new Rectangle(0, 0, Width, Height);
                var rData = resultCanvas.LockBits(rBounds, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
                Marshal.Copy(pixels, 0, rData.Scan0, pixels.Length);
                resultCanvas.UnlockBits(rData);

                pe.Graphics.DrawImageUnscaled(resultCanvas, Point.Empty);
            }
            catch { /* Internal rendering suppression */ }
        }

        private void ApplyVerticalMirror(byte[] targetArray)
        {
            int width = _sourceSnapshot.Width;
            int height = _sourceSnapshot.Height;
            int bytesPerRow = _strideLength;
            int startDrawY = Padding.Top;
            float totalRows = (height + Padding.Top);

            for (int y = startDrawY; y < height; y++)
            {
                int mirroredSourceY = (height - 1) - (y - startDrawY);
                if (mirroredSourceY < 0) break;

                int dstOffset = y * bytesPerRow;
                int srcOffset = mirroredSourceY * bytesPerRow;

                for (int x = 0; x < width; x++)
                {
                    int idx = dstOffset + (x * 4);
                    int srcIdx = srcOffset + (x * 4);

                    targetArray[idx] = targetArray[srcIdx];     // B
                    targetArray[idx + 1] = targetArray[srcIdx + 1]; // G
                    targetArray[idx + 2] = targetArray[srcIdx + 2]; // R
                    
                    // Alpha fading calculation
                    float fadeFactor = 1.0f - ((float)(y - startDrawY) / totalRows);
                    targetArray[idx + 3] = (byte)(fadeFactor * 90);
                }
            }
        }
    }
}

上記の実装において、`ReflectionProvider`はデザイナ上に配置されると、選択中のコントロールに`EnableReflection`プロパティを公開する。これを`true`に設定すると、オーバーレーコントロールが自動的に作成され、目標コントロールの移動・表示切替・再描画イベントを購読して位置同期を行う。`OnPaint`内では`DrawToBitmap`によるオフスクリーンキャプチャ後、ピクセル配列を直接操作して上下反転処理とアルファグラデーション減衰計算を実行し、ターゲットの直下に透かし描写する仕組みとなっている。

タグ: IComponent IExtenderProvider System.ComponentModel WindowsForms System.Drawing

7月24日 02:26 投稿