#title(.NETプログラミング研究 第46号) #navi(.NET プログラミング研究) #contents *.NETプログラミング研究 第46号 [#bf80fa86] **.NET質問箱 [#x2280b7c] 「.NET質問箱」では、「どぼん!のプログラミング掲示板」に書き込まれた.NETプログラミングに関する投稿を基に、さらに考察を加え、Q&A形式にまとめて紹介します。 -[[どぼん!のプログラミング掲示板>http://dobon.net/vb/bbs.html]] ***ListViewのColumnの順番を取得、設定するには? [#pb5e3966] #column(注意){{ この記事の最新版は「[[ListViewのColumnの順番を取得、設定する>http://dobon.net/vb/dotnet/control/lvcolumnorder.html]]」で公開しています。 }} ''【質問】'' ListViewコントロールのAllowColumnReorderプロパティをTrueにしてユーザーがColumnの並び替えをできるようにしたとき、並び替えられたColumnの順番を取得したいのですが、その方法が分かりません。 ''【回答】'' ListViewコントロールのColumnの順番を取得する方法は、残念ながら.NET Frameworkでは用意されていないようです。よってこの問題を解決するには、Win32 APIのSendMessage関数を使うことになりそうです。 この問題の解決法として「どぼん!のプログラミング掲示板」でピラルクさん、深山さんにより提示された方法は、LVM_GETHEADERメッセージを使ってListViewコントロールの列ヘッダに関する情報を取得するというものです。この方法に関してはこの記事の最後に示したリンク先を見ていただくことにさせていただき、ここではLVM_GETCOLUMNORDERARRAYメッセージを使った方法を紹介します。 LVM_GETCOLUMNORDERARRAYメッセージを使用した場合、Columnの順番は、ColumnHeader.Indexをインデックスとした整数値の1次元配列として取得できます。 以下にその具体例を示します。ここでは、WindowsフォームにListViewコントロール(ListView1)とボタン(Button1)が配置されており、Button1をクリックした時にListView1のColumnの順番を取得するものとします。 #code(vbnet){{ Private Declare Auto Function SendMessage Lib "user32.dll" ( _ ByVal hWnd As IntPtr, ByVal msg As Integer, _ ByVal wParam As Integer, ByVal lParam() As Integer) As Integer Private Const LVM_FIRST As Integer = &H1000 Private Const LVM_GETCOLUMNORDERARRAY As Integer = LVM_FIRST + 59 '/ <summary> '/ ListViewの列ヘッダの順番を取得する '/ </summary> '/ <param name="lv">対象とするListView</param> '/ <returns>列ヘッダの順番を示した配列</returns> Private Shared Function GetColumnHeaderOrder( _ ByVal lv As ListView) As Integer() Dim count As Integer = lv.Columns.Count Dim order(count) As Integer If SendMessage(lv.Handle, LVM_GETCOLUMNORDERARRAY, _ count, order) = 0 Then Throw New ApplicationException( _ "Columnの順番の取得に失敗しました。") End If Return order End Function }} #code(csharp){{ [DllImport("user32.dll", CharSet=CharSet.Auto)] private static extern int SendMessage( IntPtr hWnd, int msg, int wParam, int [] lParam); private const int LVM_FIRST = 0x1000; private const int LVM_GETCOLUMNORDERARRAY = (LVM_FIRST + 59); /// <summary> /// ListViewの列ヘッダの順番を取得する /// </summary> /// <param name="lv">対象とするListView</param> /// <returns>列ヘッダの順番を示した配列</returns> private static int[] GetColumnHeaderOrder(ListView lv) { int count = lv.Columns.Count; int[] order = new int[count]; if (SendMessage(lv.Handle, LVM_GETCOLUMNORDERARRAY, count, order == 0) throw new ApplicationException( "Columnの順番の取得に失敗しました。"); return order; } //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //列ヘッダの順番を取得 int[] orders = GetColumnHeaderOrder(ListView1); //列ヘッダのTextとその順番を表示する for (int i = 0; i < ListView1.Columns.Count; i++) Console.WriteLine(ListView1.Columns[i].Text + ":Position=" + orders[i].ToString()); } }} 次にListViewコントロールのColumnの順番を設定する方法を紹介します。 これは、ListViewコントロールにColumnHeaderを追加するときの順番を変えればいいだけの話ですが、上記の方法のように、LVM_SETCOLUMNORDERARRAYメッセージを使うという方法もあります。 以下にLVM_SETCOLUMNORDERARRAYメッセージを使ってListViewコントロールのColumnの順番を設定する例を示します。ここでは、WindowsフォームにListViewコントロール(ListView1)とボタン(Button2)が配置されており、Button2をクリックした時にListView1のColumnの順番をものと戻す(Index通りの順番に戻す)ようにしています。 #code(vbnet){{ Private Declare Auto Function SendMessage Lib "user32.dll" ( _ ByVal hWnd As IntPtr, ByVal msg As Integer, _ ByVal wParam As Integer, ByVal lParam() As Integer) As Integer Private Const LVM_FIRST As Integer = &H1000 Private Const LVM_SETCOLUMNORDERARRAY As Integer = LVM_FIRST + 58 '/ <summary> '/ ListViewの列ヘッダの順番を設定する '/ </summary> '/ <param name="lv">対象とするListView</param> '/ <param name="order">列ヘッダの順番を示した配列</param> Private Shared Sub SetColumnHeaderOrder( _ ByVal lv As ListView, ByVal order() As Integer) If SendMessage(lv.Handle, LVM_SETCOLUMNORDERARRAY, _ order.Length, order) = 0 Then Throw New ApplicationException( _ "Columnの順番の設定に失敗しました。") End If End Sub 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click '列ヘッダの順番を配列に入れる Dim orders(ListView1.Columns.Count - 1) As Integer Dim i As Integer For i = 0 To orders.Length - 1 orders(i) = i Next i '列ヘッダの順番を設定する SetColumnHeaderOrder(ListView1, orders) End Sub }} #code(csharp){{ [DllImport("user32.dll", CharSet=CharSet.Auto)] private static extern int SendMessage( IntPtr hWnd, int msg, int wParam, int [] lParam); private const int LVM_FIRST = 0x1000; private const int LVM_SETCOLUMNORDERARRAY = (LVM_FIRST + 58); /// <summary> /// ListViewの列ヘッダの順番を設定する /// </summary> /// <param name="lv">対象とするListView</param> /// <param name="order">列ヘッダの順番を示した配列</param> private static void SetColumnHeaderOrder(ListView lv, int[] order) { if (SendMessage(lv.Handle, LVM_SETCOLUMNORDERARRAY, order.Length, order) == 0) throw new ApplicationException( "Columnの順番の設定に失敗しました。"); } //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //列ヘッダの順番を配列に入れる int[] orders = new int[ListView1.Columns.Count]; for (int i = 0; i < orders.Length; i++) orders[i] = i; //列ヘッダの順番を設定する SetColumnHeaderOrder(ListView1, orders); } }} ○この記事の基になった掲示板のスレッド -[[ListViewで列順の変更を保持する方法 | 投稿者(敬称略) こう, ピラルク, 深山>http://dobon.net/vb/bbs/log3-4/1894.html]] ***画像をクリックして拡大、縮小表示できるようにするには? [#uea69af6] #column(注意){{ この記事の最新版は「[[ピクチャボックスの画像をクリックして拡大、縮小表示できるようにする>http://dobon.net/vb/dotnet/graphics/zoominout.html]]」で公開しています。 }} ''【質問】'' ピクチャボックスに表示した画像をマウスクリックにより、クリックした位置を中央にして、拡大(左クリックした時)、縮小(右クリックした時)させたいのですが、どのようにすればいいですか? ''【回答】'' ここでは、ピクチャボックスのPaintイベントでGraphics.DrawImageメソッドを使って画像を表示させることにします。ピクチャボックスがクリックされた時に(ここではMouseDownイベントを使います)、画像の表示倍率を決定し、ピクチャボックスで画像を表示させる範囲を計算します。ピクチャボックスのPaintイベントハンドラでは、Graphics.DrawImageメソッドを使って実際にこの範囲に画像を表示します。 それでは実際に作成してみましょう。仕様としては、テキストボックスに画像ファイル名を入力し、ボタンをクリックすると、ピクチャボックスに画像が表示されるようにします。さらに、ピクチャボックスを左クリックすると、クリックした位置がピクチャボックスの中央にし、画像が倍に拡大されて表示されます。右クリックでは、クリックした位置がピクチャボックスの中央に来るように、画像が1/2に縮小して表示します。 まずフォーム(Form1)にピクチャボックス(PictureBox1)とテキストボックス(TextBox1)とボタン(Button1)を配置します。さらに、PictureBox1のClickイベントハンドラとしてButton1_Clickを、MouseDownイベントハンドラとしてPictureBox1_MouseDownを、さらにButton1のクリックイベントハンドラとしてButton1_Clickを作成します。 これで準備は完了です。後はForm1クラスに以下のようなコードを書き込み、完成です。 #code(vbnet){{ '表示する画像 Private img As Bitmap '倍率 Private ratio As Single = 1.0F '画像を表示させる範囲 Private imgRect As Rectangle 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click '表示する画像を読み込む img = New Bitmap(TextBox1.Text) '初期化 imgRect = New Rectangle(0, 0, img.Width, img.Height) ratio = 1.0F '画像を表示する PictureBox1.Invalidate() End Sub 'PictureBox1のMouseDownイベントハンドラ Private Sub PictureBox1_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles PictureBox1.MouseDown Dim pb As PictureBox = CType(sender, PictureBox) 'クリックされた位置を画像上の位置に変換 Dim imgPoint As New Point(CInt((e.X - imgRect.X) / ratio), _ CInt((e.Y - imgRect.Y) / ratio)) '倍率を変更する If e.Button = MouseButtons.Left Then ratio *= 2.0F Else If e.Button = MouseButtons.Right Then ratio *= 0.5F End If End If '画像の表示範囲を計算する imgRect.Width = CInt(Math.Round((img.Width * ratio))) imgRect.Height = CInt(Math.Round((img.Height * ratio))) imgRect.X = CInt(Math.Round((pb.Width / 2 - imgPoint.X * ratio))) imgRect.Y = CInt(Math.Round((pb.Height / 2 - imgPoint.Y * ratio))) '画像を表示する PictureBox1.Invalidate() End Sub 'PictureBox1のPaintイベントハンドラ Private Sub PictureBox1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles PictureBox1.Paint If Not (img Is Nothing) Then '画像を指定された範囲に描画する e.Graphics.DrawImage(img, imgRect) End If End Sub }} #code(csharp){{ //表示する画像 private Bitmap img; //倍率 private float ratio = 1F; //画像を表示させる範囲 private Rectangle imgRect; //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //表示する画像を読み込む img = new Bitmap(TextBox1.Text); //初期化 imgRect = new Rectangle(0, 0, img.Width, img.Height); ratio = 1F; //画像を表示する PictureBox1.Invalidate(); } //PictureBox1のMouseDownイベントハンドラ private void PictureBox1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { PictureBox pb = (PictureBox) sender; //クリックされた位置を画像上の位置に変換 Point imgPoint = new Point( (int) Math.Round((e.X - imgRect.X) / ratio), (int) Math.Round((e.Y - imgRect.Y) / ratio)); //倍率を変更する if (e.Button == MouseButtons.Left) { ratio *= 2F; } else if (e.Button == MouseButtons.Right) { ratio *= 0.5F; } //画像の表示範囲を計算する imgRect.Width = (int) Math.Round(img.Width * ratio); imgRect.Height = (int) Math.Round(img.Height * ratio); imgRect.X = (int) Math.Round(pb.Width / 2 - imgPoint.X * ratio); imgRect.Y = (int) Math.Round(pb.Height / 2 - imgPoint.Y * ratio); //画像を表示する PictureBox1.Invalidate(); } //PictureBox1のPaintイベントハンドラ private void PictureBox1_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { if (img != null) { //画像を指定された範囲に描画する e.Graphics.DrawImage(img, imgRect); } } }} ○この記事の基になった掲示板のスレッド -[[ピクチャーボックス | 投稿者(敬称略) 水野, 琴, 管理人>http://dobon.net/vb/bbs/log3-3/1683.html]] **.NET Tips [#ob38f0b4] ***PictureBoxに表示されている画像を取得する [#q38a9e32] #column(注意){{ この記事の最新版は「[[フォーム、コントロールの外観をキャプチャする>http://dobon.net/vb/dotnet/graphics/invokepaint.html]]」で公開しています。 }} ここではInvokePaintメソッド、及びInvokePaintBackgroundメソッドを使ってPictureBoxに表示されている画像を取得する方法を紹介します。この方法で取得できる画像は、PictureBoxのPaintイベント、及びPaintBackgroundイベントでPaintEventArgs.Graphicsに描画された画像だけであり、具体的には、PictureBoxのImageプロパティ、BackGroundImageプロパティ、BackColorプロパティで指定された画像や、PaintイベントハンドラでPaintEventArgs.Graphicsを使って描画された画像などに限られます。(これ以外の方法で描画された画像を取得するには、画面をキャプチャーする必要があるでしょう。) 以下に、この方法によりPictureBox(PictureBox1)に表示されている画像を画像ファイルに保存する方法を示します。 #code(vbnet){{ 'PictureBox1の大きさを取得 Dim rect As Rectangle = PictureBox1.ClientRectangle 'PictureBox1に表示されている画像を取得するためのBitmap Dim bmp As New Bitmap(rect.Width, rect.Height) 'bmpに画像を入れるための準備 Dim g As Graphics = Graphics.FromImage(bmp) Dim pea As New PaintEventArgs(g, rect) 'PaintBackgroundイベントを発生 Me.InvokePaintBackground(PictureBox1, pea) 'Paintイベントを発生 Me.InvokePaint(PictureBox1, pea) '画像を保存する bmp.Save("C:\test.png") '後始末 g.Dispose() bmp.Dispose() }} #code(csharp){{ //PictureBox1の大きさを取得 Rectangle rect = PictureBox1.ClientRectangle; //PictureBox1に表示されている画像を取得するためのBitmap Bitmap bmp = new Bitmap(rect.Width, rect.Height); //bmpに画像を入れるための準備 Graphics g = Graphics.FromImage(bmp); PaintEventArgs pea = new PaintEventArgs(g, rect); //PaintBackgroundイベントを発生 this.InvokePaintBackground(PictureBox1, pea); //Paintイベントを発生 this.InvokePaint(PictureBox1, pea); //画像を保存する bmp.Save(@"C:\test.png"); //後始末 g.Dispose(); bmp.Dispose(); }} この方法によりPictureBox以外のコントロールの外見も画像として取得できるように期待されるかもしれませんが、実際にはほとんどのコントロールでうまく行きません。(Label、Button、DataGridコントロール等だけでうまく行くようですが、FlatStyleプロパティがSystemの時は失敗します。) ***Refresh、Update、Invalidateメソッドの違い [#v539f330] #column(注意){{ この記事の最新版は「[[Refresh、Update、Invalidateメソッドの違い>http://dobon.net/vb/dotnet/control/refreshupdateinvalidate.html]]」で公開しています。 }} ControlクラスのRefresh、Update、Invalidateメソッドは主にコントロールの再描画を促すために使用されますが、これらの違いはヘルプを読んだだけでは非常に分かりにくいです。 ヘルプによると、これらのメソッドは次のように説明されています。 Refresh : 強制的に、コントロールがクライアント領域を無効化し、直後にそのコントロール自体とその子コントロールを再描画するようにします。 Update : コントロールによって、クライアント領域内の無効化された領域が再描画されます。 Invalidate : コントロールの特定の領域を無効にし、そのコントロールに描画メッセージを送信します。 -[[Refreshメソッド>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemwindowsformscontrolclassrefreshtopic.asp]] -[[Updateメソッド>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemwindowsformscontrolclassupdatetopic.asp]] -[[Invalidateメソッド>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemwindowsformscontrolclassinvalidatetopic.asp]] これらのメソッドが実際に何をしているのか調べるには、「Reflector for .NET」などの逆アセンブラを使ってDecompileすればよいでしょう。その結果、次のようなことが分かります。 -[[Reflector for .NET>http://www.aisto.com/roeder/DotNet/]] まず、Refreshメソッドは、Invalidateメソッド(パラメータはTrue)を呼び出した後、さらにUpdateメソッドを呼び出しているだけです。 また、Invalidateメソッドは、Win32 APIのRedrawWindow(子コントロールを再描画する時)または、InvalidateRect関数を呼び出しており、Updateメソッドは、Win32 APIのUpdateWindow関数を呼び出しているらしいということが分かります。 つまり、Invalidateメソッドはアプリケーションキューが空になった時にコントロールを再描画し、Updateメソッドはアプリケーションキューが空でなくても再描画すべき領域があるならばすぐに再描画し、Refreshはアプリケーションキューが空でなくてもコントロールとその子コントロールをすぐに再描画するということになります。 言葉だけでは分かりにくいので、実際のコードでその動作を確認してみましょう。次のサンプルは、Windowsフォーム(Form1)にボタンを4つ配置し、これらをクリックすると、フォームに0から10までの数字が1秒おきにカウントアップして表示されるようにしたものです。4つのボタンはそれぞれカウンターをインクリメントした後にRefresh、Update、Invalidateメソッドを呼び出す(あるいは呼び出さない)という違いがあります。 #code(vbnet){{ 'カウンターの値 Private counter As Integer = 0 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click Dim i As Integer For i = 0 To 9 'カウンターを増やす counter += 1 'フォームを再描画しない '1秒間停止する System.Threading.Thread.Sleep(1000) Next i End Sub 'Button2のクリックイベントハンドラ Private Sub Button2_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button2.Click Dim i As Integer For i = 0 To 9 'カウンターを増やす counter += 1 'Invalidateを呼び出す Me.Invalidate() '1秒間停止する System.Threading.Thread.Sleep(1000) Next i End Sub 'Button3のクリックイベントハンドラ Private Sub Button3_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button3.Click Dim i As Integer For i = 0 To 9 'カウンターを増やす counter += 1 'Updateを呼び出す Me.Update() '1秒間停止する System.Threading.Thread.Sleep(1000) Next i End Sub 'Button4のクリックイベントハンドラ Private Sub Button4_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button4.Click Dim i As Integer For i = 0 To 9 'カウンターを増やす counter += 1 'Refreshを呼び出す Me.Refresh() '1秒間停止する System.Threading.Thread.Sleep(1000) Next i End Sub 'PictureBox1のPaintイベントハンドラ Private Sub PictureBox1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles MyBase.Paint 'カウンターを描画する e.Graphics.DrawString(counter.ToString(), Me.Font, _ Brushes.Black, 0, 0) End Sub }} #code(csharp){{ //カウンターの値 private int counter = 0; //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { for (int i = 0; i < 10; i++) { //カウンターを増やす counter++; //フォームを再描画しない //1秒間停止する System.Threading.Thread.Sleep(1000); } } //Button2のクリックイベントハンドラ private void Button2_Click(object sender, System.EventArgs e) { for (int i = 0; i < 10; i++) { //カウンターを増やす counter++; //Invalidateを呼び出す this.Invalidate(); //1秒間停止する System.Threading.Thread.Sleep(1000); } } //Button3のクリックイベントハンドラ private void Button3_Click(object sender, System.EventArgs e) { for (int i = 0; i < 10; i++) { //カウンターを増やす counter++; //Updateを呼び出す this.Update(); //1秒間停止する System.Threading.Thread.Sleep(1000); } } //Button4のクリックイベントハンドラ private void Button4_Click(object sender, System.EventArgs e) { for (int i = 0; i < 10; i++) { //カウンターを増やす counter++; //Refreshを呼び出す this.Refresh(); //1秒間停止する System.Threading.Thread.Sleep(1000); } } //PictureBox1のPaintイベントハンドラ private void PictureBox1_Paint( object sender, System.Windows.Forms.PaintEventArgs e) { //カウンターを描画する e.Graphics.DrawString( counter.ToString(), this.Font, Brushes.Black, 0, 0); } }} まず、カウンターをインクリメントした後に何もしない時(Button1をクリックした時)は、フォームに表示された数字は全く変化しません(ただし、10秒後フォームを一回隠してもう一度表示するなどすれば、カウンターの値が表示されます)。しかもこの10秒間は、フォーム全体はフリーズしたようになり、フォームの上に別のウィンドウを重ねても、フォームは一切再描画されません。 Invalidateメソッドを呼び出した時(Button2をクリックした時)も同様に10秒間変化しませんが、10秒後ようやくカウンターが更新されて表示されます。しかし、10秒間フォーム全体はフリーズしたようになります。 Updateメソッドを呼び出した時(Button3をクリックした時)も同じく何も変化しないように見えますが、フォームの上に別のウィンドウを重ね、またどけるというようなことを行うと、フォームは1秒おきに再描画されます。カウンターが表示される部分を別のウィンドウで隠せば、カウンターも更新されて表示されます。 Refreshメソッドを呼び出した時(Button4をクリックした時)は、何もしなくても1秒おきにフォーム全体が再描画され、カウンターも更新されて表示されます。 実際のこれらのメソッドの使い分けとしては、コントロールを再描画したいがすぐである必要がないときはInvalidateメソッドを使い、今すぐ再描画する必要があるときはRefreshメソッドを使うということになるでしょう。 **コメント [#la699a35] #comment //これより下は編集しないでください #pageinfo([[:Category/.NET]],2004-11-23 (火) 06:00:00,DOBON!,2010-03-21 (日) 19:49:01,DOBON!) |