ComboBox の選択後のイベントは、TextChanged イベントで拾う

落とし穴、と言いますか、やっぱり ComboBox コントロールの落とし穴っぽい動きの話です。

■問題

単純にフォームに ComboBox コントロールを貼り付けて、項目を選択した直後のイベントを拾いたい。
さてどうするのか?

という要件があって、なんか手元のコードでは、SelectedIndexChanged イベントを拾っていた訳です。
SelectedIndexChanged イベントでも悪くはないのですが、ComboBox で項目を選択している途中のイベントも拾ってしまうので、【最後のイベントだけを拾う】ってのがなかなかでき難い、というか、これでバグっているので。

■イベントは4種類ある

あらためて ComboBox のイベントを見ると、それらしいものが4つほどあります。

・SelectedIndexChanged イベント:選択項目が変更されるたびに発生する
・SelectionChangeCommitted イベント:項目がコミットするタイミングで発生する
・DropDownClosed イベント:ドロップダウンのリストが、閉じられるタイミングで発生する
・TextChanged イベント:ComboBox のテキストが変更されるタイミングで発生する

■試行錯誤してみる

↓なコードを書いて、発生するイベントを見てみます。

Private Sub ComboBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectedIndexChanged
    Debug.Print("ComboBox1_SelectedIndexChanged")
End Sub

Private Sub ComboBox1_SelectionChangeCommitted(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectionChangeCommitted
    Debug.Print("ComboBox1_SelectionChangeCommitted")
End Sub

Private Sub ComboBox1_DropDownClosed(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.DropDownClosed
    Debug.Print("ComboBox1_DropDownClosed")
End Sub

Private Sub ComboBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.TextChanged
    Debug.Print("ComboBox1_TextChanged")
End Sub

動かし方は、

・マウスで選択する。
・マウスでスクロールバーを動かす。
・キーボードでリストを選択する。
・リストを閉じたままで、キーボードで動かす。

のパターンがあります。

一見すると、SelectedIndexChanged イベントで良いような気もするのですが、リストを選択している途中もバシバシとイベントが発生するので駄目です。
マウスで動かしているときは発生しないのですが、キーボードの矢印キーで動かすと連続して発生します。

じゃあ、SelectionChangeCommitted でもいいのかな、と思っていると。
リストを閉じる時に、右にある▼をクリックすると、SelectionChangeCommitted イベントが発生しないという落とし穴…なんじゃそりゃ?状態なので、ちょっと使えません。

そうなると、DropDownClosed イベントでリストが閉じられるタイミングを拾えばよいのでは?と思うのですが、
きちんと▼を押して、リストから選択するときは良いのですが、リストを出さずに上下のカーソルキーで直接動かすときには、実は DropDownClosed イベントが発生しないのです。えぇ SelectionChangeCommitted イベントは発生するんですけどね。

そんな訳で、素直に TextChanged イベントを使うのが無難ですね…という結論でした。

ただ、TextChanged イベントの場合は、編集可能な ComboBox のときにはイベントがややこしいんですよね…悩ましい、という話

カテゴリー: 開発, VB | 4件のコメント

アリスの鞄は作った人を知っている

ちょっとアリスシリーズ風に書き下し。
要は、デバッグ用に呼び出し元のクラス名を取得したいのですが、デバッグ用なので new 時にインスタンスやクラス名を渡したくない、のですね。なので、呼び出されたクラス/メソッドのほうから、こっそりと StackFrame を使って、呼び出し元のクラス名を取得するという技です。

' 参考
' 自分自身のクラス名とメソッド名:Gushwell's C# Dev Notes
' http://gushwell.ldblog.jp/archives/50715142.html

Public Class Form1

	''' <summary>
	''' アリスを作成
	''' </summary>
	''' <param name="sender"></param>
	''' <param name="e"></param>
	''' <remarks></remarks>
	Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

		Dim alice As New Alice
		alice.Check()

	End Sub

	''' <summary>
	''' ロリータを作成
	''' </summary>
	''' <param name="sender"></param>
	''' <param name="e"></param>
	''' <remarks></remarks>
	Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

		Dim lolita As New Lolita
		lolita.Check()

	End Sub
End Class

Public Class Person

	Protected _bag As Bag

	''' <summary>
	''' コンストラクタ
	''' </summary>
	''' <remarks></remarks>
	Public Sub New()
		' バッグを作成(作成者名はバッグの内部で保存される)
		_bag = New Bag
	End Sub
	''' <summary>
	''' 作成者を表示
	''' </summary>
	''' <remarks></remarks>
	Public Sub Check()
		MessageBox.Show("class in " + _bag.GetClassName)
	End Sub

End Class

''' <summary>
''' アリスクラス
''' </summary>
''' <remarks></remarks>
Public Class Alice
	Inherits Person
End Class

''' <summary>
''' ロリータクラス
''' </summary>
''' <remarks></remarks>
Public Class Lolita
	Inherits Person
	' ※本来は、クラス名を渡すべき
	Public Sub New()
		_bag = New Bag("LOLITA")
	End Sub

End Class

Public Class Bag

	Protected _cname As String

	''' <summary>
	''' コンストラクタ
	''' </summary>
	''' <remarks></remarks>
	Public Sub New()

		Dim st As New StackTrace(False)
		' Bag -> Person -> Alice の順で 2 を指定する
		Dim sf As StackFrame = st.GetFrame(2)
		' 呼出元を保存しておく
		_cname = sf.GetMethod.ReflectedType.FullName

	End Sub

	''' <summary>
	''' 本来はクラス名を渡すべき
	''' </summary>
	''' <param name="cname"></param>
	''' <remarks></remarks>
	Public Sub New(ByVal cname As String)
		_cname = cname
	End Sub

	''' <summary>
	''' 設定されているクラス名を取得
	''' </summary>
	''' <returns></returns>
	''' <remarks></remarks>
	Public Function GetClassName() As String
		Return _cname
	End Function
End Class

Alice クラス内で、new Bag をしています。この Bag を生成したのは誰か?というのは、本来ならば Alice のインスタンスか、クラス名を渡す、あるいは、Bag プロパティに設定する、ということをやる必要があるのですが、これをスタックフレームを使って呼び出し元のクラス名を取得するようにします。

Bag クラスのコンストラクタの部分が少しトリッキーなことになっています。

	Public Sub New()

		Dim st As New StackTrace(False)
		' Bag -> Person -> Alice の順で 2 を指定する
		Dim sf As StackFrame = st.GetFrame(2)
		' 呼出元を保存しておく
		_cname = sf.GetMethod.ReflectedType.FullName

	End Sub

StackTrace クラスでスタックトレースを取得した後、GetFrame メソッドで呼び出し元を取得します。このときに、スタックの状態が、Bag -> Person -> Alice になっているので「2」を指定しています。
間に Person クラスが挟まるのは、Alice クラスとの継承関係があるからです。なので、フォームからの呼び出しをチェックする場合も、Form クラスを継承していることを考慮にいれて、GetFrame メソッドに渡す値を調節しないといけません。ややこしいですね…というか、実装依存になるので、ピンポイントでしか使えない技です。なので、素直にクラス名を渡したほうがよさそうです。

こういう場合、XmlDocument クラスのようにファクトリーパターンを使います。コンストラクタにクラス名を渡すよりは自然かも。

Dim bag = Bag.CreateInstanceWithName("Lolita")
'あるいは
Dim bag As New Bag("Lolita")

■参考

自分自身のクラス名とメソッド名:Gushwell’s C# Dev Notes
http://gushwell.ldblog.jp/archives/50715142.html

カテゴリー: 開発, VB | アリスの鞄は作った人を知っている はコメントを受け付けていません

参照設定しているアセンブリからクラスを動的にロードする方法

Assembly.LoadFrom を使って別のアセンブリにあるフォームを起動する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2312

上記なところで、Assembly.LoadFrom メソッドを使うと動的にアセンブリをロードすることができる、って話をしましたが、実はプラグインでなければ【動的】である必要はないわけで、あらかじめ【参照設定】などでアセンブリを指定しておく方法もあります。

つまりは、

Dim cname as string = "クラス名"
Dim t as Type = Type.GetType( cname )
Dim f as Form = System.Activator.CreateInstance(t)

な感じで、Type から直接作ればよいわけです。

実は前回、なんでわざわざ動的ロードにしたのかなぁ、と思っていたら、この cname が参照設定先の別のアセンブリにあると、単純にクラス名(例えば、FormSub とか)だけではオブジェクトを作成できなかったからなのです。

アセンブリの構成が、

SampleMain.exe
+ FormMain

のように単一のアセンブリに入っていれば、そのまま Type.GetType(“FormMain”) でロードができるのですが、

SampleMain.exe
+ FormMain
SampleSub.exe
+ FormSub

のように、Main.exe から Sub.exe を【参照設定】している場合は、Main.exe の中では、Type.GetType(“FormSub”) ができません。
別アセンブリの場合は、【修飾名】が必要って訳です。

で、この修飾名はどうやって取り出すかというと、AssemblyQualifiedName で取り出します。

Type.GetType メソッド (String) (System)
http://msdn.microsoft.com/ja-jp/library/w3f99sx1(v=VS.80).aspx
Type.AssemblyQualifiedName プロパティ (System)
http://msdn.microsoft.com/ja-jp/library/system.type.assemblyqualifiedname(v=VS.80).aspx

なにやら、ややこしいことが書いてありますが、要は試しのプログラムを作って AssemblyQualifiedName プロパティで取り出してみればば良いわけです。下記のように試してみます。

Dim frm as new FormSub
Debug.Print( frm.GetType.AssemblyQualifiedName )

すると、「FormSub, SampleSub, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null」のように修飾名が取れるので、そのままクラス名として利用します。

Dim cname as string = "FormSub, SampleSub, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
Dim t as Type = Type.GetType( cname )
Dim f as Form = System.Activator.CreateInstance(t)

ロードする対象のクラスを切り替えたいとき(画面を切り替えたいとき)は、クラス名の「FormSub」を変えれば ok ですね。

カテゴリー: 開発, VB | 1件のコメント

Windows 8 Preview に色々インストールしてみる

まずは、Firefox。インストール時に「対応していないよ」と警告がでるが動く。

おつぎは Eclips. 久しく eclips を使っていないので java のプロジェクトが手元にないw。 android の開発には必須ですね。

OpenOffice も入れてみる。メニューのフォントが変なのは、前からなのか?多分、フォントが決め打ちになっているのかな。

色々インストールすると、windows 8 のスタートページはごちゃごちゃになります。まあ、「すべてのプログラム」のアイコンがぶちまけられるので仕方がない?

IE のタブレット版(だっけ)では、Windows Azure Platform が動かないので、

通常版の IE10 で動かすとか

ちなみに、Visual Studio 11 + Windows 8 では、Windows Azure Template がインストールされない…のです。Visual Web Developer 2010 Express を要求するのでインストール中。

カテゴリー: 開発, 雑談 | Windows 8 Preview に色々インストールしてみる はコメントを受け付けていません

iOS 本の共著してくださる方を募集します

なかなか、皆さんタイミングが合わないようで(意味不明)、広くブログで募集してみます。
# 今月末ぐらいまでは募集したいのですが、都合によっては早く締め切るかも。

某社の「iOS 逆引き」本(って伏字になってない気もしますがw)を書くために、iOS/iPhone/iPad のプログラミングができる方かつ執筆できる方を募集します。iPhone/iPad のプログラミング本です。
全体が、500 個ぐらいの tips を作る予定なので、はじめての執筆であれば 100 tips とか、あるいは元気な方(謎)であれば 200 tips かというところでしょうか。そのあたりは、調節したいと思います。内容は、これから決める、という泥縄方式ですから。

逆引きの本の場合は、ひとつひとつが単発なので1冊の本を書き続けるよりは(執筆)初心者向き、と言いますか分担がしやすいんですよね。

そんな訳で、

■募集内容

某社の iOS 逆引きの共同執筆を手伝ってくださる方(平たく言えば「共著者」)を1名募集します。
tips 数は、100 から 200 の間ぐらい。

■締め切り

来年の2月末ぐらい。

■募集期間

今月末ぐらい(9月末)まで。
応募者多数の場合は、早めに締め切ることがあります(というか、応募者0名の場合は、それはそれで早く締め切るかもw)

■応募先

メール masuda_at_moonmile.net(_at_ を @ に変えてください)や、ツイッター @moonmile までお願いします。
このブログのコメントや問い合わせフォームでも構いません。
連絡先(メールでokです)と執筆への意気込み、を書いてくださればOKです。

カテゴリー: 開発 | iOS 本の共著してくださる方を募集します はコメントを受け付けていません

WebMatrix 再見…というか、テンプレートからサイトを作成してみる

2011-09-17 – まめしば雑記
http://d.hatena.ne.jp/shiba-yan/20110917

なところで、WebMatrix 2 のベータ版が出たそうで、というのを知って、そういえば WebMatrix ってどこまで使えるんだろうと既にインストール済みの(苦笑)WebMatrix を立ち上げてみて、ちょっと驚き。

WebMatrix を起動して、「テンプレートからサイトを作成する」をクリックします。

[ベーカリー]を選択

どうやらテンプレートで出来たらしいので、[実行]ボタンをクリック

ブラウザが立ち上がって、ベーカリーサイトが出来上がっているという具合。

いや、中身は空っぽなんでしょうけど、「今すぐ注文」ボタンで遷移するところまでできています。

で、改めて、どんなファイルができているかと覗いてみると、

Razor テンプレートを使っていて、データベースにアクセスしていて、という出来上がり具合。素直にすごいッ!!!

で、何が驚きなのかというと、

Rasor なのに ASP.NET MVC 3 を使っていない。なるほど、通常の *.aspx の代わりに Razor 部分を使うっていう手もあるんですね…と言いますか、これ本家(?)の Visual Studio 2010 では簡単には作れないのです。空の ASP.NET アプリケーションを作ってから Razor を使うように設定して、という具合に、いくつかの手順を踏まないと作れない。だけど、WebMatrix のほうは、一気に作ってしまう、という反則技(笑)。

で、ASP.NET MVC でややこしいのは(CakePHPでも同じですが)、コントローラーやらビューやらという仕組みを覚えないといけない。PHP や *.aspx で直接がしがし書くほうが手軽だったりする。でも、このベーカリーのテンプレートだと、Razor の文法だけ流用して MVC のところが無い、ってのが潔いのです。その手があったか、という感じ(今更ですが)。

WebMatrix 2 になるとインテリセンスが効くので、結構コーディングがしやすそう。Visual Studio や Web Developer と使い分けながら PHP 絡みでコーディングすると有効なのかも、と考えたり。

カテゴリー: 開発, ASP.NET | WebMatrix 再見…というか、テンプレートからサイトを作成してみる はコメントを受け付けていません

Visual Studio 11 では VC++ のインテリセンスが効くよ

MSDN から Visual Studio 11 preview 版(VS2012候補ですね)を入れて、Visual C++ を試してみました。
画像は後から貼り付けますが、インテリセンスが効くようになったよッ!!! ってことで。

これで、まともに C++/CLI でコーディングができそうです。

# VS 2010 にもサービスパック経由で出して欲しいなぁと。

カテゴリー: 開発, C++ | Visual Studio 11 では VC++ のインテリセンスが効くよ はコメントを受け付けていません

Windows Azure Tools for Microsoft Visual Studio v1.5 リリース

Windows Azure SDK and Tools | Windows Azure Platform
http://www.microsoft.com/windowsazure/sdk/

予想はしていたことでしたが…って予想していなかったよッ!!!
先月の頭に v1.4 が出てから今の時期に v1.5 って、うーむ。執筆中の書籍は v1.5 で画面キャプチャをし直しですかね。日本語版がいつ出るかにかかっていますが。

私としては、

– Goblal.aspx.cs へのコード挿入をテンプレート時点でして欲しい。
– web.config の sessionState を削除して欲しい。

ってところです。

後で、v1.5 英語版で試してみるということで。

カテゴリー: 開発, Azure | 1件のコメント

DataGridView へのバインドが遅い場合は、RowHeadersWidthSizeMode プロパティの値を疑ってみよう

DataGrid への表示は、VB6 の頃から遅くて、表示更新をしないと早くなるという噂(けど真実)があったりします。
で、.NET になって DataGridView への DataSource プロパティへのバインドをすると早くなる、ってのが定番なんですが(セルへちまちま貼り付けるよりも早くなります…が、測定はしてないので、そのうちに)、なぜか、DataSource プロパティへのバインドをしているのに、とてつもなく遅くなる現象が発覚したので、晒します。

1.10 カラムある DataGridView を作ります。
2.データを作成

/// <summary>
/// データ作成
/// </summary>
/// <returns></returns>
private List<Data> MakeData()
{
    List<Data> lst = new List<Data>();
    for (int i = 0; i < 3000; i++)
    {
        Data d = new Data();
        d.Col1 = i.ToString();
        d.Col2 = DateTime.Now.ToString();
        d.Col3 = DateTime.Now.ToString();
        d.Col4 = DateTime.Now.ToString();
        d.Col5 = DateTime.Now.ToString();
        d.Col6 = DateTime.Now.ToString();
        d.Col7 = DateTime.Now.ToString();
        d.Col8 = DateTime.Now.ToString();
        d.Col9 = DateTime.Now.ToString();
        d.Col10= DateTime.Now.ToString();
        lst.Add(d);
    }
    return lst;

3.データバインド

private void button1_Click(object sender, EventArgs e)
{
    var lst = MakeData();
    // 自動でカラムを作らない
    dataGridView1.AutoGenerateColumns = false; 
    // 列幅はそのまま
    dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.EnableResizing;

    Stopwatch sw = new Stopwatch();
    sw.Start();
    dataGridView1.DataSource = lst;
    sw.Stop();
    MessageBox.Show(string.Format("経過時間:{0} msec", sw.ElapsedMilliseconds));
    // 10 msec 程度

}

普通に作るととても早いのですが…

dataGridView1.RowHeadersWidthSizeMode = 
	DataGridViewRowHeadersWidthSizeMode.AutoSizeToAllHeaders;

このように、AutoSizeToAllHeaders を指定して列幅を自動で作成しようとすると。

private void button1_Click(object sender, EventArgs e)
{
    var lst = MakeData();
    // 自動でカラムを作らない
    dataGridView1.AutoGenerateColumns = false; 
    // 列幅を自動調節する
    dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.AutoSizeToAllHeaders;

    Stopwatch sw = new Stopwatch();
    sw.Start();
    dataGridView1.DataSource = lst;
    sw.Stop();
    MessageBox.Show(string.Format("経過時間:{0} msec", sw.ElapsedMilliseconds));
    // 4 分経ってもまだ終わりません。
}

ってな具合に、1000 倍ぐらい遅くなります。

想像するに、DataSource プロパティでバインドしたデータが、1 件加わるごとに AutoSizeToAllHeaders で列幅を調節している感じなんですよね。なので、一度、EnableResizing で計算しないようにしてから、後で AutoSizeToAllHeaders を指定します。

private void button1_Click(object sender, EventArgs e)
{
    var lst = MakeData();
    // 自動でカラムを作らない
    dataGridView1.AutoGenerateColumns = false; 
    // 列幅はそのままに変える
    dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.EnableResizing;

    Stopwatch sw = new Stopwatch();
    sw.Start();
    dataGridView1.DataSource = lst;
    // 全て入力した後に列幅を自動調節する
    dataGridView1.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.AutoSizeToAllHeaders;
    sw.Stop();
    MessageBox.Show(string.Format("経過時間:{0} msec", sw.ElapsedMilliseconds));
    // 108 msec 高速に動作する
}

カテゴリー: 開発, C# | 2件のコメント

ホットキーで指定したウィンドウをキャプチャする

前回、Ctrl+PrintScreen でアクティブなウィンドウをキャプチャする | Moonmile Solutions Blog を書いている時に、あらかじめ指定したウィンドウの画面キャプチャをする、というのも書いたので晒しておきます。

どうも CopyFromScreen を使ったり、hDC を使って画面キャプチャをすると、Visual Studio 2010 のメニューがキャプチャできないんですよね…仕方がないので、SendKeys クラスを使ってキーエミュレートをします。

キャプチャ画面はこんな感じ。メインウィンドウに乗っている場合、そのままキャプチャします。

■メインウィンドウを持つプロセスの一覧を取得

まずは、ウィンドウを持つプロセス一覧を取って、hWnd を保存しておきます。

///
/// プロセス一覧を取得
///
/// <param name="sender" />
/// <param name="e" />
private void button3_Click(object sender, EventArgs e)
{
    listBox1.Items.Clear();
    foreach (Process p in Process.GetProcesses())
    {
        if (p.MainWindowHandle != IntPtr.Zero)
        {
            Debug.Print(p.MainWindowTitle);
            ProcessWin pw = new ProcessWin();
            pw.Title = p.MainWindowTitle;
            pw.hWnd = p.MainWindowHandle;
            listBox1.Items.Add(pw);
        }
    }
}

■リストで選択してキャプチャ

///
/// Window Proc のオーバーライド
///
/// <param name="message" />
protected override void WndProc(ref Message message)
{
    if (message.Msg == WM_HOTKEY && (int)message.WParam == HOTKEY_ID)
    {
        if (checkBox1.Checked == false)
        {
        	// PrtSc キーを送信
            SendKeys.SendWait("^{PRTSC}");
            return;
        }
        else
        {
            if (listBox1.SelectedIndex == -1) return;
            ProcessWin pw = (ProcessWin)listBox1.SelectedItem;
            Bitmap bmp = WindowCapture(pw.hWnd);
            Clipboard.SetImage(bmp);
            return;
        }
    }
    base.WndProc(ref message);
}
///
/// 指定したウィンドウをキャプチャ
///
///
private Bitmap WindowCapture( IntPtr hWnd )
{
    IntPtr hDC = GetWindowDC(hWnd);
    //ウィンドウの大きさを取得
    RECT rect = new RECT();
    GetWindowRect(hWnd, ref rect);
    Bitmap bmp = new Bitmap(rect.right - rect.left, rect.bottom - rect.top);
    // キーエミュレート
    SendKeys.SendWait("^{PRTSC}");
    Application.DoEvents();
    Image img = Clipboard.GetImage();
    if (img != null)
    {
    	// クリップボードから指定画面を切り取る
        Graphics g = Graphics.FromImage(bmp);
        g.DrawImage(img, -rect.left, -rect.top);
    }
    return bmp;
}

これをホットキーの【Ctrl】+【PrtSc】に設定しておけば ok です。

カテゴリー: 開発, C# | ホットキーで指定したウィンドウをキャプチャする はコメントを受け付けていません