.NETプログラミング研究 第46号 †
.NET質問箱 †
「.NET質問箱」では、「どぼん!のプログラミング掲示板」に書き込まれた.NETプログラミングに関する投稿を基に、さらに考察を加え、Q&A形式にまとめて紹介します。
ListViewのColumnの順番を取得、設定するには? †
【質問】
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の順番を取得するものとします。
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
| | 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
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
|
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
| | [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);
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;
}
private void Button1_Click(object sender, System.EventArgs e)
{
int[] orders = GetColumnHeaderOrder(ListView1);
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通りの順番に戻す)ようにしています。
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
| | 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
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
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
|
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
| | [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);
private static void SetColumnHeaderOrder(ListView lv, int[] order)
{
if (SendMessage(lv.Handle, LVM_SETCOLUMNORDERARRAY,
order.Length, order) == 0)
throw new ApplicationException(
"Columnの順番の設定に失敗しました。");
}
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);
}
|
○この記事の基になった掲示板のスレッド
画像をクリックして拡大、縮小表示できるようにするには? †
【質問】
ピクチャボックスに表示した画像をマウスクリックにより、クリックした位置を中央にして、拡大(左クリックした時)、縮小(右クリックした時)させたいのですが、どのようにすればいいですか?
【回答】
ここでは、ピクチャボックスのPaintイベントでGraphics.DrawImageメソッドを使って画像を表示させることにします。ピクチャボックスがクリックされた時に(ここではMouseDownイベントを使います)、画像の表示倍率を決定し、ピクチャボックスで画像を表示させる範囲を計算します。ピクチャボックスのPaintイベントハンドラでは、Graphics.DrawImageメソッドを使って実際にこの範囲に画像を表示します。
それでは実際に作成してみましょう。仕様としては、テキストボックスに画像ファイル名を入力し、ボタンをクリックすると、ピクチャボックスに画像が表示されるようにします。さらに、ピクチャボックスを左クリックすると、クリックした位置がピクチャボックスの中央にし、画像が倍に拡大されて表示されます。右クリックでは、クリックした位置がピクチャボックスの中央に来るように、画像が1/2に縮小して表示します。
まずフォーム(Form1)にピクチャボックス(PictureBox1)とテキストボックス(TextBox1)とボタン(Button1)を配置します。さらに、PictureBox1のClickイベントハンドラとしてButton1_Clickを、MouseDownイベントハンドラとしてPictureBox1_MouseDownを、さらにButton1のクリックイベントハンドラとしてButton1_Clickを作成します。
これで準備は完了です。後はForm1クラスに以下のようなコードを書き込み、完成です。
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
45
46
47
48
49
50
51
52
53
54
55
56
| | Private img As Bitmap
Private ratio As Single = 1.0F
Private imgRect As Rectangle
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
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
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
|
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| | private Bitmap img;
private float ratio = 1F;
private Rectangle imgRect;
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();
}
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();
}
private void PictureBox1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
if (img != null)
{
e.Graphics.DrawImage(img, imgRect);
}
}
|
○この記事の基になった掲示板のスレッド
.NET Tips †
PictureBoxに表示されている画像を取得する †
ここではInvokePaintメソッド、及びInvokePaintBackgroundメソッドを使ってPictureBoxに表示されている画像を取得する方法を紹介します。この方法で取得できる画像は、PictureBoxのPaintイベント、及びPaintBackgroundイベントでPaintEventArgs.Graphicsに描画された画像だけであり、具体的には、PictureBoxのImageプロパティ、BackGroundImageプロパティ、BackColorプロパティで指定された画像や、PaintイベントハンドラでPaintEventArgs.Graphicsを使って描画された画像などに限られます。(これ以外の方法で描画された画像を取得するには、画面をキャプチャーする必要があるでしょう。)
以下に、この方法によりPictureBox(PictureBox1)に表示されている画像を画像ファイルに保存する方法を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| | Dim rect As Rectangle = PictureBox1.ClientRectangle
Dim bmp As New Bitmap(rect.Width, rect.Height)
Dim g As Graphics = Graphics.FromImage(bmp)
Dim pea As New PaintEventArgs(g, rect)
Me.InvokePaintBackground(PictureBox1, pea)
Me.InvokePaint(PictureBox1, pea)
bmp.Save("C:\test.png")
g.Dispose()
bmp.Dispose()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| | Rectangle rect = PictureBox1.ClientRectangle;
Bitmap bmp = new Bitmap(rect.Width, rect.Height);
Graphics g = Graphics.FromImage(bmp);
PaintEventArgs pea = new PaintEventArgs(g, rect);
this.InvokePaintBackground(PictureBox1, pea);
this.InvokePaint(PictureBox1, pea);
bmp.Save(@"C:\test.png");
g.Dispose();
bmp.Dispose();
|
この方法によりPictureBox以外のコントロールの外見も画像として取得できるように期待されるかもしれませんが、実際にはほとんどのコントロールでうまく行きません。(Label、Button、DataGridコントロール等だけでうまく行くようですが、FlatStyleプロパティがSystemの時は失敗します。)
Refresh、Update、Invalidateメソッドの違い †
ControlクラスのRefresh、Update、Invalidateメソッドは主にコントロールの再描画を促すために使用されますが、これらの違いはヘルプを読んだだけでは非常に分かりにくいです。
ヘルプによると、これらのメソッドは次のように説明されています。
Refresh : 強制的に、コントロールがクライアント領域を無効化し、直後にそのコントロール自体とその子コントロールを再描画するようにします。
Update : コントロールによって、クライアント領域内の無効化された領域が再描画されます。
Invalidate : コントロールの特定の領域を無効にし、そのコントロールに描画メッセージを送信します。
これらのメソッドが実際に何をしているのか調べるには、「Reflector for .NET」などの逆アセンブラを使ってDecompileすればよいでしょう。その結果、次のようなことが分かります。
まず、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メソッドを呼び出す(あるいは呼び出さない)という違いがあります。
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| | Private counter As Integer = 0
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
System.Threading.Thread.Sleep(1000)
Next i
End Sub
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
Me.Invalidate()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
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
Me.Update()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
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
Me.Refresh()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
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
|
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| | private int counter = 0;
private void Button1_Click(object sender, System.EventArgs e)
{
for (int i = 0; i < 10; i++)
{
counter++;
System.Threading.Thread.Sleep(1000);
}
}
private void Button2_Click(object sender, System.EventArgs e)
{
for (int i = 0; i < 10; i++)
{
counter++;
this.Invalidate();
System.Threading.Thread.Sleep(1000);
}
}
private void Button3_Click(object sender, System.EventArgs e)
{
for (int i = 0; i < 10; i++)
{
counter++;
this.Update();
System.Threading.Thread.Sleep(1000);
}
}
private void Button4_Click(object sender, System.EventArgs e)
{
for (int i = 0; i < 10; i++)
{
counter++;
this.Refresh();
System.Threading.Thread.Sleep(1000);
}
}
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メソッドを使うということになるでしょう。
コメント †