#title(.NETプログラミング研究 第42号)

#navi(.NET プログラミング研究)

#contents

*.NETプログラミング研究 第42号 [#lbbc4a2b]

**.NET Tips [#uc68e5e1]

***フォームが一つしか表示されないようにする / VB6と同様にフォームにアクセスできるようにする [#ocde6ce4]

#column(注意){{
この記事の最新版は「[[フォームが一つしか表示されないようにする>http://dobon.net/vb/dotnet/form/singleform.html]]」で公開しています。
}}

Visual Basic 6.0以前のユーザーにとって.NETプログラミングの第一の関門はWindowsフォームの表示に関する問題でしょう。VB6では、デザイナでフォームを作成し、コードで

(フォーム名).Show

とするだけでフォームを表示できました。しかも、同じフォームのShowメソッドを何回呼び出してもフォームが何枚も表示されるということがありません。

これに対して.NETでは、newによりフォームのインスタンスを作成してからShowメソッドを呼び出す必要があり、さらに、「newしてShow」を繰り返していると、それだけ次々とフォームが開かれることになります。

このような事態を避け、VB6のようにフォームを扱えるようにするためには、静的プロパティを使用するようにします。フォームのインスタンスは静的フィールドに保持しておき、静的プロパティでこのフィールドがnullまたは破棄(Dispose)されているときに新しいインスタンスを作成するようにします。

次に簡単なサンプルを示します。Form2というフォームクラスがあるものとし、以下のような_instanceフィールドとInstanceプロパティを加えます。(これに加え、Form2クラスをシールクラスとし、Form2のコンストラクタをprivateに変更すればさらに確実です。)

#code(vbnet){{
'ただ一つのフォームのインスタンスを保持するフィールド
Private Shared _instance As Form2

'ただ一つのフォームにアクセスするためのプロパティ
Public Shared ReadOnly Property Instance() As Form2
    Get
        '_instanceがnullまたは破棄されているときは、
        '新しくインスタンスを作成する
        If _instance Is Nothing OrElse _instance.IsDisposed Then
            _instance = New Form2
        End If
        Return _instance
    End Get
End Property
}}

#code(csharp){{
//ただ一つのフォームのインスタンスを保持するフィールド
private static Form2 _instance;

//ただ一つのフォームにアクセスするためのプロパティ
public static Form2 Instance
{
	get
	{
		//_instanceがnullまたは破棄されているときは、
		//新しくインスタンスを作成する
		if (_instance == null || _instance.IsDisposed)
			_instance = new Form2();
		return _instance;
	}
}
}}

Form2のインスタンスにアクセスするには、Form2.Instance静的メソッドを使用します。例えば、Form2をモードレスで表示するには、

Form2.Instance.Show()

とするだけです(名前空間が異なる場合は、適当な名前空間を付加してください)。Form2.Instance.Show()を何回呼び出しても、フォームが何枚も表示されることはありません。(このプロパティはスレッドセーフではありませんが、それほど問題にならないでしょう。しかしどうしてもスレッドセーフにしたいということであれば、下の例を参考にしてください。)

ところで、この問題の解決に、シングルトン(Singleton)デザインパターンがそのまま使えるのではないかと考える方もいらっしゃるかもしれません。シングルトンとは、クラスのインスタンスがただ一つであることを保障するためのデザインパターンで、フォームのインスタンスがただ一つであることを保障するということであれば、これはまさに「ビンゴ」と言えるでしょう。

シングルトン及び、C#におけるシングルトンの実装に関しては、次のページが参考になります。

-[[Microsoft patterns & practices Patterns - シングルトン>http://www.microsoft.com/japan/msdn/practices/type/Patterns/enterprise/DesSingleton.asp]]
-[[C# でのシングルトンの実装>http://www.microsoft.com/japan/msdn/practices/type/Patterns/enterprise/ImpSingletonInCsharp.asp]]
-[[Implementing the Singleton Pattern in C#>http://www.yoda.arachsys.com/csharp/singleton.html]]

ここでは「C# でのシングルトンの実装」の「静的な初期化」で紹介されている方法を使用します。この方法によりインスタンスが一つであるForm2フォームを作成するには、まずForm2クラスをシールクラスとし、Form2のコンストラクタをprivateに変更し、以下のような_instanceフィールドとInstanceプロパティを加えます。

#code(vbnet){{
'基本クラスとして使用できないようにする
Public NotInheritable Class Form2
    Inherits System.Windows.Forms.Form

	'コンストラクタをPrivateにする
    Private Sub New()
		'(省略)
    End Sub

	'(省略)

    'フォームのインスタンスを保持するフィールド
    Private Shared _instance As New Form2

    'フォームにアクセスするためのプロパティ
    Public Shared ReadOnly Property Instance() As Form2
        Get
            Return _instance
        End Get
    End Property
End Class
}}

#code(csharp){{
//基本クラスとして使用できないようにする
public sealed class Form2 : System.Windows.Forms.Form
{
	//コンストラクタをPrivateにする
	private Form2()
	{
		//(省略)
	}

	//(省略)

	//フォームのインスタンスを保持するフィールド
	private static readonly Form2 _instance = new Form2();

	//フォームにアクセスするためのプロパティ
	public static Form2 Instance
	{
		get
		{
			return _instance;
		}
	}
}
}}

ところが上記のようにフォームクラスにシングルトンパターンをそのまま適用すると、うまく行きません。フォームを閉じてから再びフォームを開こうとすると、エラーが発生するのです。というのは、フォームのインスタンスが一つであっても、そのフォームが破棄されてしまったらそのインスタンスを使ってフォームを開くことができなくなってしまうからです。

この問題を解決するには、一つの方法として、フォームが破棄されないようにフォームが閉じられないようにするといった対策が考えられます。

例えば、Form2クラスに次のコードを書き加えることにより、フォームを閉じようとした時に閉じずに、隠すようになります。

#code(vbnet){{
'フォームを閉じずに隠すようにする
Protected Overrides Sub OnClosing( _
    ByVal e As System.ComponentModel.CancelEventArgs)
    e.Cancel = True
    Me.Hide()
End Sub
}}

#code(csharp){{
//フォームを閉じずに隠すようにする
protected override void OnClosing(CancelEventArgs e)
{
	e.Cancel = true;
	this.Hide();
}
}}

***フォームのリサイズが終了するまでコントロールの大きさを変えない [#g7f5e9aa]

#column(注意){{
この記事の最新版は「[[フォームのリサイズが終了するまでコントロールの大きさを変えない>http://dobon.net/vb/dotnet/form/preventcontrolresize.html]]」で公開しています。
}}

Windowsフォームのサイズがユーザーにより変更でき、フォームに配置されたコントロールがDockやAnchorプロパティによりそのサイズ(また配置)がフォームのサイズとともに変更されるようになっている時、フォームの境界線をマウスでドラッグすることによりフォームのサイズを変えると、ドラッグしている間絶え間なくコントロールのサイズが変化します。しかし、ドラッグしている間はコントロールの配置が変化せずに、ドラッグが完了してマウスボタンを放したときに初めてコントロールの配置が変化するようにしたいというケースもあるでしょう。

そのような場合の対策について、ニュースグループに適切な投稿がありますので、ここではそれを紹介します。

-[[Newsgroups:microsoft.public.dotnet.framework.windowsforms | Subject:Re: resize finished event | From:Shawn Burke [MSFT]>http://groups.google.co.jp/groups?hl=ja&lr=&ie=UTF-8&inlang=ja&selm=uRawvh9vBHA.1572%40tkmsftngp07]]

ここでは2つの方法が紹介されています。まず一つ目が、Application.Idleイベントを使用する方法です。フォームのサイズを変更している最中はApplication.Idleイベントが発生せず、変更完了してから発生するという特徴を利用しています。

この方法によるコードは次のようになります。フォームクラス内に記述してください。(ニュースグループに紹介されている方法を多少変えてあります。良くなっているかは分かりませんが...。)

#code(vbnet){{
'OnResizeのEventArgsを保持する
Dim resizeEA As EventArgs = Nothing

'フォームのサイズが変更した時
Protected Overrides Sub OnResize(ByVal e As EventArgs)
    If resizeEA Is Nothing Then
        resizeEA = e
        AddHandler Application.Idle, AddressOf OnIdle
    End If
End Sub

'アプリケーションがアイドル状態になった時
Private Sub OnIdle(ByVal s As Object, ByVal e As EventArgs)
    If Not (resizeEA Is Nothing) Then
        '基本クラスのOnResizeを呼び出す
        MyBase.OnResize(resizeEA)
        resizeEA = Nothing
        RemoveHandler Application.Idle, AddressOf OnIdle
    End If
End Sub
}}

#code(csharp){{
//OnResizeのEventArgsを保持する
EventArgs resizeEA = null; 

//フォームのサイズが変更した時
protected override void OnResize(EventArgs e)
{
	if (resizeEA == null)
	{
		resizeEA = e;
		Application.Idle += new EventHandler(OnIdle);
	}
}

//アプリケーションがアイドル状態になった時
private void OnIdle(object s, EventArgs e)
{
	if (resizeEA != null)
	{
		//基本クラスのOnResizeを呼び出す
		base.OnResize(resizeEA);
		resizeEA = null;
		Application.Idle -= new EventHandler(OnIdle);
	}
}
}}

2つ目の方法は、フォームのWndProcをオーバーライドし、WM_EXITSIZEMOVEメッセージを待ち、送られてきた時にコントロールを配置するという方法です。

ニュースグループで紹介されているコードはそのまま使用しても何も起こりません。私が思うに、例えば、OnResizeメソッドをオーバーライドし、何もしないという処理が必要であるにもかかわらず、説明されていないようです(リサイズが普通に行われてしまっては、全く意味がありませんので)。

よって、正しく動作するコードは、例えば次のようになるでしょう。

#code(vbnet){{
'OnResizeをオーバーライドし、何もしない
Protected Overrides Sub OnResize(ByVal e As EventArgs)
End Sub

Private Const WM_EXITSIZEMOVE As Integer = &H232

Protected Overrides Sub WndProc(ByRef m As Message)
    If m.Msg = WM_EXITSIZEMOVE Then
        'リサイズ終了後、コントロールを配置する
        Invalidate()
        PerformLayout()
    End If
    MyBase.WndProc(m)
End Sub
}}

#code(csharp){{
//OnResizeをオーバーライドし、何もしない
protected override void OnResize(EventArgs e)
{
}

private const int WM_EXITSIZEMOVE = 0x232;

protected override void WndProc(ref Message m) 
{
	if (m.Msg == WM_EXITSIZEMOVE) 
	{
		//リサイズ終了後、コントロールを配置する
		Invalidate();
		PerformLayout();
	}
	base.WndProc(ref m);
}
}}

***MDI親フォームの背景色を変更する [#le4ddcb8]

#column(注意){{
この記事の最新版は「[[MDI親フォームの背景色を変更する>http://dobon.net/vb/dotnet/form/mdibackcolor.html]]」で公開しています。
}}

WindowsフォームのIsMdiContainerプロパティをtrueにすることによりMDI親フォームとすると、フォームのBackColorプロパティは無視され、子フォームが表示されるくぼんだクライアント領域はシステムで指定された色(大抵は濃い灰色)になります。

このクライアント領域には、実はMdiClientというコントロールがあり、MdiClientコントロールのBackColorプロパティを変更することにより、クライアント領域の色を変えることができます。(ただし、ヘルプによると、MdiClientクラスは「独自に作成したコード内で直接使用することはできません。」とありますので、この方法が適切であるかは保障できません。)

まずフォームにあるMdiClientコントロールを探す次のような静的メソッドを作成します。

#code(vbnet){{
''' <summary>
''' フォームのMdiClientコントロールを探して返す
''' </summary>
''' <param name="f">MdiClientコントロールを探すフォーム</param>
''' <returns>見つかったMdiClientコントロール</returns>
Public Shared Function GetMdiClient( _
    ByVal f As System.Windows.Forms.Form) _
    As System.Windows.Forms.MdiClient
    Dim c As System.Windows.Forms.Control
    For Each c In f.Controls
        If TypeOf c Is System.Windows.Forms.MdiClient Then
            Return CType(c, System.Windows.Forms.MdiClient)
        End If
    Next c
    Return Nothing
End Function
}}

#code(csharp){{
/// <summary>
/// フォームのMdiClientコントロールを探して返す
/// </summary>
/// <param name="f">MdiClientコントロールを探すフォーム</param>
/// <returns>見つかったMdiClientコントロール</returns>
public static System.Windows.Forms.MdiClient 
	GetMdiClient(System.Windows.Forms.Form f)
{
	foreach (System.Windows.Forms.Control c in f.Controls)
		if (c is System.Windows.Forms.MdiClient)
			return (System.Windows.Forms.MdiClient) c;
	return null;
}
}}

このメソッドを使って、MDI親フォームのクライアント領域にフォームのBackColorプロパティで指定された色を適用するコードは次のようになります。
(MDI親フォームのクラス内に記述するものとします。)

#code(vbnet){{
'MdiClientを探す
Dim mc As System.Windows.Forms.MdiClient = GetMdiClient(Me)
If Not (mc Is Nothing) Then
    '背景色を変更し、再描画する
    mc.BackColor = Me.BackColor
    mc.Invalidate()
End If
}}

#code(csharp){{
//MdiClientを探す
System.Windows.Forms.MdiClient mc = GetMdiClient(this);
if (mc != null)
{
	//背景色を変更し、再描画する
	mc.BackColor = this.BackColor;
	mc.Invalidate();
}
}}

***MDI親フォームの背景を描画する [#x939139e]

#column(注意){{
この記事の最新版は「[[MDI親フォームの背景を描画する>http://dobon.net/vb/dotnet/form/mdibackgroundimage.html]]」で公開しています。
}}

MDI親フォームの背景に画像を表示するには、フォームのBackGroundプロパティが使えます。先に紹介したBackColorプロパティとは違い、BackGroundプロパティに指定された画像はクライアント領域に正常に表示されます(画像はタイル状に表示されます)。

MDI親フォームの背景を独自に描画する方法は、"vbAccelerator"で紹介されています。

-[[vbAccelerator - Painting in the MDI Client Area>http://vbaccelerator.com/home/NET/Code/Libraries/Windows/MDI_Client_Area_Painting/article.asp]]

vbAcceleratorで紹介されている方法は、Win32 APIを使う方法で、かなり複雑です。

このように難しい方法を使わなくても、先に紹介したMdiClientコントロールのPaintイベントを使ってMDI親フォームのクライアント領域を描画すれば、もっと簡単に実現できます。

以下に紹介するサンプルでは、MDI親フォームのクライアント領域に画像("back.bmp")をクライアント領域の大きさに合わせて描画するようにしています。なお、このコードはMDI親フォームに記述されており、ParentForm_LoadはフォームのLoadイベントハンドラとして登録されているものとします。

#code(vbnet){{
 '背景に表示する画像
Dim back As New Bitmap("back.bmp")

'MDI親フォームのLoadイベントハンドラ
Private Sub ParentForm_Load(sender As Object, _
	e As System.EventArgs) Handles MyBase.Load
	Me.IsMdiContainer = True
	'MdiClientの取得
	Dim mc As System.Windows.Forms.MdiClient = GetMdiClient(Me)
	'MdiClientのPaintとResizeイベントハンドラを追加
	AddHandler mc.Paint, AddressOf MdiClient_Paint
	AddHandler mc.Resize, AddressOf MdiClient_Resize
End Sub

Private Sub MdiClient_Paint(sender As Object, e As PaintEventArgs)
	Dim mc As System.Windows.Forms.MdiClient = _
		CType(sender, System.Windows.Forms.MdiClient)
	'画像をクライアント領域にあわせて描画する
	e.Graphics.DrawImage(back, mc.ClientRectangle)
End Sub

Private Sub MdiClient_Resize(sender As Object, e As EventArgs)
	Dim mc As System.Windows.Forms.MdiClient = _
		CType(sender, System.Windows.Forms.MdiClient)
	'Paintイベントを呼び出す
	mc.Invalidate()
End Sub

    ''' <summary>
    ''' フォームのMdiClientコントロールを探して返す
    ''' </summary>
    ''' <param name="f">MdiClientコントロールを探すフォーム</param>
    ''' <returns>見つかったMdiClientコントロール</returns>
    Public Shared Function GetMdiClient( _
        ByVal f As System.Windows.Forms.Form) _
        As System.Windows.Forms.MdiClient
        Dim c As System.Windows.Forms.Control
        For Each c In f.Controls
            If TypeOf c Is System.Windows.Forms.MdiClient Then
                Return CType(c, System.Windows.Forms.MdiClient)
            End If
        Next c
        Return Nothing
    End Function
}}

#code(csharp){{
//背景に表示する画像
Bitmap back = new Bitmap("back.bmp");

//MDI親フォームのLoadイベントハンドラ
private void ParentForm_Load(object sender, System.EventArgs e)
{
	this.IsMdiContainer = true;
	//MdiClientの取得
	System.Windows.Forms.MdiClient mc = GetMdiClient(this);
	//MdiClientのPaintとResizeイベントハンドラを追加
	mc.Paint += new PaintEventHandler(MdiClient_Paint);
	mc.Resize += new EventHandler(MdiClient_Resize);
}

private void MdiClient_Paint(object sender, PaintEventArgs e)
{
	System.Windows.Forms.MdiClient mc =
		(System.Windows.Forms.MdiClient) sender;
	//画像をクライアント領域にあわせて描画する
	e.Graphics.DrawImage(back, mc.ClientRectangle);
}

private void MdiClient_Resize(object sender, EventArgs e)
{
	System.Windows.Forms.MdiClient mc =
		(System.Windows.Forms.MdiClient) sender;
	//Paintイベントを呼び出す
	mc.Invalidate();
}

/// <summary>
/// フォームのMdiClientコントロールを探して返す
/// </summary>
/// <param name="f">MdiClientコントロールを探すフォーム</param>
/// <returns>見つかったMdiClientコントロール</returns>
public static System.Windows.Forms.MdiClient 
	GetMdiClient(System.Windows.Forms.Form f)
{
	foreach (System.Windows.Forms.Control c in f.Controls)
		if (c is System.Windows.Forms.MdiClient)
			return (System.Windows.Forms.MdiClient) c;
	return null;
}
}}

**コメント [#q56fb68d]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2004-09-28 (火) 06:00:00,DOBON!,2010-03-21 (日) 19:37:08,DOBON!)
[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]