#title(.NETプログラミング研究 第42号) #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!) |