NativeWindowクラスを用いたサブクラス化(スナップウィンドウの作成)
Visual Basicでプログラミングをしたことのある方なら”サブクラス化”という言葉を聞いた事があるのでは ないでしょうか。これはかつてVisual Basicを使っていて、「こんなことができればいいのに!」という 思いを実現するために多くの方が使っていた手法だと思います。ここではそれをC#で行う方法についての話です。
.NETでは継承でも解決できるが…
Visual Basic6.0つまりVBが.NETになる以前ではまだ継承が完全にはサポートされていませんでした。 従って、サブクラス化などという泥臭いことを強いられていたわけですが、 VB.NETでは完全にオブジェクト指向がサポートされたとこにより、継承も思う存分使うことができるように なりました。そうなると例えば、Formクラスなどを継承して、WndProc()メソッドをオーバーライドすることによって 、サブクラス化と同様の結果を得ることができます。これはC#についても同様です。
それで十分ではないかと思われるかもしれません。確かに継承という機構は非常に 強力なため多くの場合問題は解決されるでしょう。しかし、ある程度作ってしまったコントロール クラスに何かしらの機能を付け加えたいという場合や、同時に二つ以上の機能を付加したい場合 などには、わざわざ継承を用いるのは面倒です。こんなときにはフォームにコントロールを貼り付けるような 感覚で機能が追加できたらいいと思われます。このような要望に答えてくれ得るのがサブクラス化でしょう。
Win32APIで直接行うサブクラス化は危険
では、どのようにしてサブクラス化を行えばいいのでしょうか?VB6.0などでは、SetWindowLong()やCallWindowProc() などのWin32 APIを直接呼び出して行っていました。確かに.NETにおいてもこのような方法でサブクラス化を実現できます。 事実このあと紹介するNativeWindowクラスでも内部ではこれらのWin32 APIを使用しています。しかし、ガベージコレクション機能や、 関数ポインタが単なるプロキシである等複雑な状況下にあるマネージコードにおいては、 色々と考慮にいれて非常に高度な技術を要すると思われます。
サブクラス化はNativeWindowクラスで行う
以上のことから、マネージ環境においてはサブクラス化用に予め用意されているNativeWindowクラスを用いるのが 最良の選択であるということです。このクラスの使い方はこちらのMSDNライブラリにサンプルつきで説明がありますので、 ここでは以下のようなサンプルを作ってみました。
スナップウィンドウを作成する
WinAmpなどでおなじみのスナップ機能をフォームに付加するサンプルです。今回のサンプルプログラムでは 画面左端に吸着するようになっています。
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
#endregion
namespace NativeWindow_Subclassing
{
/// <summary>
/// Form1 の概要の説明です。
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
/// <summary>
/// 必要なデザイナ変数です。
/// </summary>
private System.ComponentModel.Container components = null;
private NewNativeWindow nnwWindowPrc;
#region コンストラクタ
public Form1()
{
//
// Windows フォーム デザイナ サポートに必要です。
//
InitializeComponent();
nnwWindowPrc = new NewNativeWindow(this);
}
#endregion
#region Dispose処理
/// <summary>
/// 使用されているリソースに後処理を実行します。
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#endregion
#region Windows フォーム デザイナで生成されたコード
/// <summary>
/// デザイナ サポートに必要なメソッドです。このメソッドの内容を
/// コード エディタで変更しないでください。
/// </summary>
private void InitializeComponent()
{
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 12);
this.ClientSize = new System.Drawing.Size(264, 273);
this.Name = "Form1";
this.Text = "Form1";
}
#endregion
#region メインエントリポイント
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
#endregion
}
#region NewNativeWindowクラス
public class NewNativeWindow : NativeWindow
{
#region Win32
private const int WM_MOVING = 0x0216;
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
#endregion
private Form frmParent;
private Point pntDragCenter = new Point(int.MaxValue, int.MaxValue);
private int intDisplacement = 0;
private const int cintSnapableGap = 10;
public NewNativeWindow(Form parent)
{
parent.HandleCreated += new EventHandler(this.OnHandleCreated);
parent.HandleDestroyed+= new EventHandler(this.OnHandleDestroyed);
this.frmParent = parent;
}
internal void OnHandleCreated(object sender, EventArgs e)
{
AssignHandle(((Form)sender).Handle);
}
internal void OnHandleDestroyed(object sender, EventArgs e)
{
ReleaseHandle();
}
protected override void WndProc(ref Message m)
{
if(m.Msg == WM_MOVING)
{
if(frmParent.Left <= cintSnapableGap)
{
if(Cursor.Position.X - pntDragCenter.X >0) ++intDisplacement;
RECT r = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
if(intDisplacement <= cintSnapableGap)
{
r.left = 0;
r.right = frmParent.Width;
pntDragCenter = Cursor.Position;
}
else
{
r.left = cintSnapableGap + 1;
r.right = r.left + frmParent.Width;
intDisplacement = 0;
pntDragCenter = new Point(int.MaxValue, int.MaxValue);
}
Marshal.StructureToPtr(r, m.LParam, false);
}
}
base.WndProc(ref m);
}
}
#endregion
}