.NETプログラミング研究 第42号 †
.NET Tips †
フォームが一つしか表示されないようにする / VB6と同様にフォームにアクセスできるようにする †
Visual Basic 6.0以前のユーザーにとって.NETプログラミングの第一の関門はWindowsフォームの表示に関する問題でしょう。VB6では、デザイナでフォームを作成し、コードで
(フォーム名).Show
とするだけでフォームを表示できました。しかも、同じフォームのShowメソッドを何回呼び出してもフォームが何枚も表示されるということがありません。
これに対して.NETでは、newによりフォームのインスタンスを作成してからShowメソッドを呼び出す必要があり、さらに、「newしてShow」を繰り返していると、それだけ次々とフォームが開かれることになります。
このような事態を避け、VB6のようにフォームを扱えるようにするためには、静的プロパティを使用するようにします。フォームのインスタンスは静的フィールドに保持しておき、静的プロパティでこのフィールドがnullまたは破棄(Dispose)されているときに新しいインスタンスを作成するようにします。
次に簡単なサンプルを示します。Form2というフォームクラスがあるものとし、以下のような_instanceフィールドとInstanceプロパティを加えます。(これに加え、Form2クラスをシールクラスとし、Form2のコンストラクタをprivateに変更すればさらに確実です。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| | Private Shared _instance As Form2
Public Shared ReadOnly Property Instance() As Form2
Get
If _instance Is Nothing OrElse _instance.IsDisposed Then
_instance = New Form2
End If
Return _instance
End Get
End Property
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| | private static Form2 _instance;
public static Form2 Instance
{
get
{
if (_instance == null || _instance.IsDisposed)
_instance = new Form2();
return _instance;
}
}
|
Form2のインスタンスにアクセスするには、Form2.Instance静的メソッドを使用します。例えば、Form2をモードレスで表示するには、
Form2.Instance.Show()
とするだけです(名前空間が異なる場合は、適当な名前空間を付加してください)。Form2.Instance.Show()を何回呼び出しても、フォームが何枚も表示されることはありません。(このプロパティはスレッドセーフではありませんが、それほど問題にならないでしょう。しかしどうしてもスレッドセーフにしたいということであれば、下の例を参考にしてください。)
ところで、この問題の解決に、シングルトン(Singleton)デザインパターンがそのまま使えるのではないかと考える方もいらっしゃるかもしれません。シングルトンとは、クラスのインスタンスがただ一つであることを保障するためのデザインパターンで、フォームのインスタンスがただ一つであることを保障するということであれば、これはまさに「ビンゴ」と言えるでしょう。
シングルトン及び、C#におけるシングルトンの実装に関しては、次のページが参考になります。
ここでは「C# でのシングルトンの実装」の「静的な初期化」で紹介されている方法を使用します。この方法によりインスタンスが一つであるForm2フォームを作成するには、まずForm2クラスをシールクラスとし、Form2のコンストラクタをprivateに変更し、以下のような_instanceフィールドとInstanceプロパティを加えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| | Public NotInheritable Class Form2
Inherits System.Windows.Forms.Form
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| | public sealed class Form2 : System.Windows.Forms.Form
{
private Form2()
{
}
private static readonly Form2 _instance = new Form2();
public static Form2 Instance
{
get
{
return _instance;
}
}
}
|
ところが上記のようにフォームクラスにシングルトンパターンをそのまま適用すると、うまく行きません。フォームを閉じてから再びフォームを開こうとすると、エラーが発生するのです。というのは、フォームのインスタンスが一つであっても、そのフォームが破棄されてしまったらそのインスタンスを使ってフォームを開くことができなくなってしまうからです。
この問題を解決するには、一つの方法として、フォームが破棄されないようにフォームが閉じられないようにするといった対策が考えられます。
例えば、Form2クラスに次のコードを書き加えることにより、フォームを閉じようとした時に閉じずに、隠すようになります。
1
2
3
4
5
6
| | Protected Overrides Sub OnClosing( _
ByVal e As System.ComponentModel.CancelEventArgs)
e.Cancel = True
Me.Hide()
End Sub
|
1
2
3
4
5
6
| | protected override void OnClosing(CancelEventArgs e)
{
e.Cancel = true;
this.Hide();
}
|
フォームのリサイズが終了するまでコントロールの大きさを変えない †
Windowsフォームのサイズがユーザーにより変更でき、フォームに配置されたコントロールがDockやAnchorプロパティによりそのサイズ(また配置)がフォームのサイズとともに変更されるようになっている時、フォームの境界線をマウスでドラッグすることによりフォームのサイズを変えると、ドラッグしている間絶え間なくコントロールのサイズが変化します。しかし、ドラッグしている間はコントロールの配置が変化せずに、ドラッグが完了してマウスボタンを放したときに初めてコントロールの配置が変化するようにしたいというケースもあるでしょう。
そのような場合の対策について、ニュースグループに適切な投稿がありますので、ここではそれを紹介します。
ここでは2つの方法が紹介されています。まず一つ目が、Application.Idleイベントを使用する方法です。フォームのサイズを変更している最中はApplication.Idleイベントが発生せず、変更完了してから発生するという特徴を利用しています。
この方法によるコードは次のようになります。フォームクラス内に記述してください。(ニュースグループに紹介されている方法を多少変えてあります。良くなっているかは分かりませんが...。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| | 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
MyBase.OnResize(resizeEA)
resizeEA = Nothing
RemoveHandler Application.Idle, AddressOf OnIdle
End If
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| | 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)
{
base.OnResize(resizeEA);
resizeEA = null;
Application.Idle -= new EventHandler(OnIdle);
}
}
|
2つ目の方法は、フォームのWndProcをオーバーライドし、WM_EXITSIZEMOVEメッセージを待ち、送られてきた時にコントロールを配置するという方法です。
ニュースグループで紹介されているコードはそのまま使用しても何も起こりません。私が思うに、例えば、OnResizeメソッドをオーバーライドし、何もしないという処理が必要であるにもかかわらず、説明されていないようです(リサイズが普通に行われてしまっては、全く意味がありませんので)。
よって、正しく動作するコードは、例えば次のようになるでしょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| | 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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| | 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親フォームの背景色を変更する †
WindowsフォームのIsMdiContainerプロパティをtrueにすることによりMDI親フォームとすると、フォームのBackColorプロパティは無視され、子フォームが表示されるくぼんだクライアント領域はシステムで指定された色(大抵は濃い灰色)になります。
このクライアント領域には、実はMdiClientというコントロールがあり、MdiClientコントロールのBackColorプロパティを変更することにより、クライアント領域の色を変えることができます。(ただし、ヘルプによると、MdiClientクラスは「独自に作成したコード内で直接使用することはできません。」とありますので、この方法が適切であるかは保障できません。)
まずフォームにあるMdiClientコントロールを探す次のような静的メソッドを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| | 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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| | 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親フォームのクラス内に記述するものとします。)
1
2
3
4
5
6
7
| | Dim mc As System.Windows.Forms.MdiClient = GetMdiClient(Me)
If Not (mc Is Nothing) Then
mc.BackColor = Me.BackColor
mc.Invalidate()
End If
|
1
2
3
4
5
6
7
8
| | System.Windows.Forms.MdiClient mc = GetMdiClient(this);
if (mc != null)
{
mc.BackColor = this.BackColor;
mc.Invalidate();
}
|
MDI親フォームの背景を描画する †
MDI親フォームの背景に画像を表示するには、フォームのBackGroundプロパティが使えます。先に紹介したBackColorプロパティとは違い、BackGroundプロパティに指定された画像はクライアント領域に正常に表示されます(画像はタイル状に表示されます)。
MDI親フォームの背景を独自に描画する方法は、"vbAccelerator"で紹介されています。
vbAcceleratorで紹介されている方法は、Win32 APIを使う方法で、かなり複雑です。
このように難しい方法を使わなくても、先に紹介したMdiClientコントロールのPaintイベントを使ってMDI親フォームのクライアント領域を描画すれば、もっと簡単に実現できます。
以下に紹介するサンプルでは、MDI親フォームのクライアント領域に画像("back.bmp")をクライアント領域の大きさに合わせて描画するようにしています。なお、このコードはMDI親フォームに記述されており、ParentForm_LoadはフォームのLoadイベントハンドラとして登録されているものとします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| | Dim back As New Bitmap("back.bmp")
Private Sub ParentForm_Load(sender As Object, _
e As System.EventArgs) Handles MyBase.Load
Me.IsMdiContainer = True
Dim mc As System.Windows.Forms.MdiClient = GetMdiClient(Me)
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)
mc.Invalidate()
End Sub
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| | Bitmap back = new Bitmap("back.bmp");
private void ParentForm_Load(object sender, System.EventArgs e)
{
this.IsMdiContainer = true;
System.Windows.Forms.MdiClient mc = GetMdiClient(this);
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;
mc.Invalidate();
}
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;
}
|
コメント †