none
C#(Winforms)でGoogleサジェスト風のTextBoxを実装する方法 RRS feed

  • 質問

  • いつもお世話になります。
    立て続けの投稿、また長文をご容赦ください。

    Googleサジェスト(ってもう言わないのかな?)風のTextBoxをC#(Winforms)で実装しようと思っております。
    ASP.NET、WPFでのサンプル等は見当たりますが、Winformsではあまり見かけないのでご質問とあわせて投稿させていただきました。

    ■ Googleサジェスト対応のTextBox
    長文になりますが、Googleサジェストに対応したTextBoxのコードを掲載させていただきます。
    TextBoxを継承して下記TextBoxSuggestクラスを作成しました。

    (環境はVS2010、.net 4、C#です)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using System.Windows.Forms;
    using System.Web;
    using System.Net;
    using System.Xml.Linq;
    using System.Xml;
    
    namespace WindowsFormsApplication1
    {
        /// <summary>
        /// Googleサジェスト対応のTextBox
        /// </summary>
        public class TextBoxSuggest : TextBox
        {
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public TextBoxSuggest()
            {
                this.TextChanged += new EventHandler(TextBoxSuggest_TextChanged);
                this.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;
                this.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource;
            }
            
            /// <summary>
            /// テキストが変更された時
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void TextBoxSuggest_TextChanged(object sender, EventArgs e)
            {
                AutoCompleteStringCollection ac = new AutoCompleteStringCollection();
                ac.AddRange(request(this.Text).ToArray());
                this.AutoCompleteCustomSource = ac;
            }
    
            /// <summary>
            /// GoogleのAPIにリクエスト
            /// </summary>
            /// <param name="query"></param>
            /// <returns></returns>
            protected List<string> request(string query)
            {
                List<string> ret = new List<string>();
    
                try
                {
                    if (!string.IsNullOrEmpty(query))
                    {
                        XDocument xdoc = null;
                        string strxml = string.Empty;
                        string apiurl =
                            "http://www.google.com/complete/search?hl=ja&output=toolbar&q=" +
                            HttpUtility.UrlEncode(query);
    
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(apiurl);
                        IWebProxy proxy = req.Proxy;
                        if (proxy != null)
                        {
                            proxy.Credentials = CredentialCache.DefaultCredentials;
                        }
    
                        using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
                        {
                            strxml = (new System.IO.StreamReader(res.GetResponseStream(), Encoding.Default)).ReadToEnd();
                        }
                        xdoc = XDocument.Parse(strxml);
    
                        ret =
                            (
                            from
                                c
                            in
                                xdoc.Descendants("CompleteSuggestion")
                            select
                                (c.Element("suggestion") != null && c.Element("suggestion").Attribute("data") != null ? c.Element("suggestion").Attribute("data").Value : string.Empty)
                            ).ToList<string>();
                    }
                }
                catch
                {
                    ret.Clear();
                }
    
                return ret;
            }
        }
    }


    ちなみに、GoogleサジェストのAPIを下記URLで呼び出すと
    http://www.google.com/complete/search?hl=ja&output=toolbar&q=a
    下記のようなXML形式のレスポンスを得られます。 
    <?xml version="1.0"?>
    <toplevel>
     <CompleteSuggestion>
      <suggestion data="amazon"/>
      <num_queries int="3130000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="ameba"/>
      <num_queries int="652000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="au"/>
      <num_queries int="3640000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="ana"/>
      <num_queries int="874000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="akb48"/>
      <num_queries int="129000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="アメブロ"/>
      <num_queries int="87900000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="apple"/>
      <num_queries int="1720000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="朝日新聞"/>
      <num_queries int="36800000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="嵐"/>
      <num_queries int="106000000"/>
     </CompleteSuggestion>
     <CompleteSuggestion>
      <suggestion data="アスクル"/>
      <num_queries int="2390000"/>
     </CompleteSuggestion>
    </toplevel>

    ■ 問題点
    上記は一応それらしく動作しますが、下記問題も抱えています。
    これらの問題について何か心当たりありましたら対応方法などご教示いただけたらと思います。

    ・文字入力からAPI呼び出しが動作するまでの時間を指定できない
    現状だと一文字入力するごとにTextChangedイベントが発生して、その度にGoogleサジェストAPIの呼び出し&候補文字列の更新が行われてしまいます。
    これを、例えば文字入力後1秒間のインターバルが発生した時のみAPI呼び出し&候補更新が行われるようにしたいと思っています。
    ASP.NET AJAXのAutoCompleteExtender.CompletionIntervalに該当するような機能を実装できたらと思っています。

    ・一文字ずつしか入力できない
    説明が難しいのですが・・・
    日本語入力の時のみに発生する問題です。
    例えば「あいうえお」と続けて入力したとします。
    その後Enterキーを押して「あいうえお」の入力を決定します。
    すると何故かTextBox上には「あ」のみしか入力できていません。
    そして、Googleサジェストの候補として表示されるものも「あいうえお」の物ではなく「あ」の物となります。
    望む動作は、「あいうえお」と入力・決定すれば当然ながらTextBox上には「あいうえお」が表示され「あいうえお」の候補が表示されるというものです。

    ・入力が決定されないと候補が表示されない
    上記の「一文字ずつしか入力できない」と重なるところもありますが。
    「あいうえお」と日本語入力した場合、このテキストがEnterキーで決定されないと候補が表示されません。
    実際のGoogleでは、未決定の文字列が入力されている最中にも都度候補が表示されます。
    「あ」と入力しそれが未決定の状態でも「あ」の候補が表示される。
    未決定のままそこに続けて「い」と入力すると「あい」の候補が表示される。
    といった動作にしたいと思っています。

    ・GoogleサジェストAPIから取得した候補が全て表示されない
    例えば「a」の候補はGoogleサジェストAPIからは上記XMLのように10個取得できます。
    しかし、その10個をAutoCompleteCustomSourceに設定してもこの10個が全て表示されるわけではありません。
    AutoCompleteMode.Suggest自体に独自の候補判別機能のようなものが実装されていて、AutoCompleteCustomSourceの中からさらにフィルタ・ソートされたものだけが表示されるようになっているようです。
    AutoCompleteCustomSourceに設定した候補を全てその通りに表示するようにしたいと思っています。


    各問題に対する対応方法、または全体的に見て「そもそもそれじゃ無理だよ。こっちのほうがいいよ」といったご意見などあればお聞かせいただけたら幸いです。

    よろしくお願いいたします。

     


    • 編集済み MSDN-TK 2012年2月29日 4:55
    2012年2月29日 4:50

回答

  • まず、TextChangedイベントがどのようなタイミングで発せられるか確認していますか?

    Debug.WriteLine(((TextBox)sender).Text);

    とか書いてデバッグするだけでわかる話ですが。それで見てみたところ、「あいう」と入力してEnterを押したら「あ」「あい」「あいう」の3回のイベントが連続して発生しました。つまりそういうことです。

    UIスレッドでgoogleへ候補をダウンロードしに行くのも問題でしょう。別スレッドで実行すべきです。また、期待されているのはIME確定後ではなく、文字入力最中、例えば変換候補が出ている最中にもサジェストしたいのではありませんか? そうなってくると、TextBox.TextChangedイベントではなく、IMEから変換最中の文字列を取得せざるを得ないのでは? と思います。更には、変換中にサジェスト候補を選択したらIME側は変換の中断も必要ですよね?

    • 回答としてマーク MSDN-TK 2012年2月29日 7:05
    • 回答としてマークされていない MSDN-TK 2012年2月29日 7:06
    • 回答としてマーク 山本春海 2012年4月12日 8:13
    2012年2月29日 7:01
  •  以前、「IME で入力中の文字列を知りたい」の様な質問があった気がしたので、上の検索ボックスで「ime」を検索しました。結果から、ImmSetCompositionString を見つけました。しかし、今回は「取りたい」ので、「設定したい」は当てはまりません。そこで左の関数一覧を見ると、ImmGetCompositionString がありました。こいつをキーにもう一度検索すると、「テキストボックスで漢字名を入力した際にルビを振る方法について」が見つかりました。ここに、trapemiyaさんが書かれたコードがあるので、これを利用して作ってみることにします。新しい Windows Form アプリケーションでこれだけ実行し(いや、これだけだと実行できないけど)、動作の確認を行ってください。その他、佐佑理さんが指摘されている内容も考慮の上、実装してください。一文字ずつしか入力できないのは、数秒おいてが実装できれば解決すると思いますよ。

    
    using System;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    
    namespace Practice11
    {
        public partial class UserControl1 : TextBox
        {
            public UserControl1()
            {
                InitializeComponent();
            }
    
            [DllImport("Imm32.dll", CharSet = CharSet.Auto)]
            public static extern int ImmGetContext(IntPtr hWnd);
            [DllImport("Imm32.dll")]
            public static extern int ImmGetCompositionString(
                int hIMC,
                int dwIndex,
                IntPtr lpBuf,
                int dwBufLen
                );
            [DllImport("Imm32.dll", CharSet = CharSet.Auto)]
            public static extern bool ImmReleaseContext(IntPtr hWnd, int hIMC);
    
            // これは、WndProc で m.Msg を表示し、文字入力後に発生しているメッセージを総当たりした。
            const int WM_IME_NOTIFY = 0x0282;
            // これは、ヘッダファイルから適当にあたりをつけた。
            const int GCS_COMPSTR   = 0x0008;
    
            protected override void WndProc(ref Message m)
            {
                if (m.Msg == WM_IME_NOTIFY)
                {
                    if (((int)m.LParam & GCS_COMPSTR) > 0)
                    {
                        IntPtr buffer = IntPtr.Zero;
                        int Imc = ImmGetContext(this.Handle);
                        try
                        {
                            int bytes = ImmGetCompositionString(Imc, GCS_COMPSTR, IntPtr.Zero, 0);
                            buffer = Marshal.AllocHGlobal(bytes);
                            ImmGetCompositionString(Imc, GCS_COMPSTR, buffer, bytes);
                            byte[] arrByte = new Byte[bytes];
                            Marshal.Copy(buffer, arrByte, 0, bytes);
                            string str = Encoding.GetEncoding(932).GetString(arrByte);
    
                            // ここを、適切に変更する
                            System.Diagnostics.Debug.WriteLine(str);
                        }
                        finally
                        {
                            ImmReleaseContext(this.Handle, Imc);
                            Marshal.FreeHGlobal(buffer);
                        }
    
                    }
                }
                base.WndProc(ref m);
            }
        }
    }
    

    Jitta@わんくま同盟

    • 回答としてマーク 山本春海 2012年4月12日 8:13
    2012年2月29日 13:55
  • 内容からして、IAutoComplete を使用して提案型の補完機能を提供したいということでしょうか。

    既にご自身でも確認されていますが、IAutoComplete は入力された語を含む単語を補完する機能なので、a を入力したら a が含まれている候補を絞り込むのが目的です。a から連想される語を候補としてあげることはできません。

    また、ソースは全ての候補を格納していることを前提にしているので、入力内容にあわせえて切り替えるのではなく、入力されうるすべての候補を事前に格納しておくことが想定されています。

    Windows Vista/7 では、IAutoComplete2 のオプション機能で ACO_NOPREFIXFILTERING を on にすれば、入力内容にかかわらず、常に補完ソースのすべての値を表示する(絞り込まないで、探すだけになる)ようになるので、こちらを使うと a という入力から「朝日新聞」をドロップダウンさせることができるようになります。(プロパティからは指定できないので、COM や API のメソッドを直接呼び出して設定する必要があります)

    入力内容にあわせて補完リストを変更するために、IEnumString を実装することになると思いますが、最初に書いているようにすべての入力に対する補完候補を事前に持っていることが前提の設計なので、入力にあわせて再列挙してくれるかどうかがポイントですね。たぶんしないと思いますが、IEnumString を実装して確認してみれば結果はすぐにでると思います。


    http://msdn.microsoft.com/en-us/library/windows/desktop/bb776292(v=vs.85).aspx
    2012年3月2日 4:02

すべての返信

  • >・文字入力からAPI呼び出しが動作するまでの時間を指定できない

    これについてはTextChangedイベントハンドラ内でタイマーを動かし、1秒以内の連続して入力された文字をバッファに貯めておいて、1秒間何も入力されなくなった時にAPIを呼びに行けば良いのではないでしょうか?

    >・一文字ずつしか入力できない

    は、「・入力が決定されないと候補が表示されない」との関連あるかもしれませんが、IMEから直接情報(変換中の文字など)を取ることになると思います。

    >GoogleサジェストAPIから取得した候補が全て表示されない

    同じ値の場合は1つしか表示されないのはわかるのですが、勝手にねぐることは無いように思います。requestの戻り値のListに、10個含まれていることは確認されているのでしょうか?


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2012年2月29日 5:43
    モデレータ
  • trapemiya様

    さっそくのご投稿をまことに恐れ入ります。
    残念ながら現在開発環境が手元にないため、分かる範囲内でご返答させていただきます。

    >・文字入力からAPI呼び出しが動作するまでの時間を指定できない

    これについてはTextChangedイベントハンドラ内でタイマーを動かし、1秒以内の連続して入力された文字をバッファに貯めておいて、1秒間何も入力されなくなった時にAPIを呼びに行けば良いのではないでしょうか?

    やはりTimerを使用する方法がベストなのかもしれませんね。
    もしかしたらTextChangedイベントハンドラの使用自体が適切ではないのかと思い皆様のご意見をお伺いいたしました。

    >・一文字ずつしか入力できない

    は、「・入力が決定されないと候補が表示されない」との関連あるかもしれませんが、IMEから直接情報(変換中の文字など)を取ることになると思います。

    「入力が決定されないと候補が表示されない」の対応方法としてIMEから直接情報を取るというのは何となく分かります。
    しかし、「一文字ずつしか入力できない」の問題はそれとは別と判断しておりますがこの認識は誤りでしょうか?
    ちなみに優先度的には
    一文字ずつしか入力できない>入力が決定されないと候補が表示されない
    になります。

    >GoogleサジェストAPIから取得した候補が全て表示されない

    同じ値の場合は1つしか表示されないのはわかるのですが、勝手にねぐることは無いように思います。requestの戻り値のListに、10個含まれていることは確認されているのでしょうか?

    はい、以前調べたときにはしっかりと10個含まれていました(そのはずです。。。)

    http://msdn.microsoft.com/ja-jp/library/cc708904.aspx
    こちらのページを見ていただけると分かりますが、AutoCompleteModeがSuggestに設定されている場合はAutoCompleteCustomSourceのコレクションから候補が選択されて表示される(AutoCompleteCustomSourceのコレクションが全て表示されるわけではない)ようです。
    ですのでこの動作自体は正常な動作かと思っています。

    AutoCompleteCustomSourceのコレクションを必ず全て表示するような方法があればと思っています。
    2012年2月29日 6:16
  • まず、TextChangedイベントがどのようなタイミングで発せられるか確認していますか?

    Debug.WriteLine(((TextBox)sender).Text);

    とか書いてデバッグするだけでわかる話ですが。それで見てみたところ、「あいう」と入力してEnterを押したら「あ」「あい」「あいう」の3回のイベントが連続して発生しました。つまりそういうことです。

    UIスレッドでgoogleへ候補をダウンロードしに行くのも問題でしょう。別スレッドで実行すべきです。また、期待されているのはIME確定後ではなく、文字入力最中、例えば変換候補が出ている最中にもサジェストしたいのではありませんか? そうなってくると、TextBox.TextChangedイベントではなく、IMEから変換最中の文字列を取得せざるを得ないのでは? と思います。更には、変換中にサジェスト候補を選択したらIME側は変換の中断も必要ですよね?

    • 回答としてマーク MSDN-TK 2012年2月29日 7:05
    • 回答としてマークされていない MSDN-TK 2012年2月29日 7:06
    • 回答としてマーク 山本春海 2012年4月12日 8:13
    2012年2月29日 7:01
  • 佐祐理様

    ご投稿をまことに恐れ入ります。

    まず、TextChangedイベントがどのようなタイミングで発せられるか確認していますか?

    Debug.WriteLine(((TextBox)sender).Text);

    とか書いてデバッグするだけでわかる話ですが。それで見てみたところ、「あいう」と入力してEnterを押したら「あ」「あい」「あいう」の3回のイベントが連続して発生しました。つまりそういうことです。


    情報をありがとうございます。
    開発環境が現在手元にないためこれを試せないでいました。
    今日の夜にでも実際に確認してみます。

    自分で確かめられる環境がない中で疑問点ばかりあげさせていただき大変恐縮なのですが。。。
    なぜ三回のイベントが連続して発生しているのに最終的に「あ」の候補が表示されてしまっているのでしょうか?
    上記ソースコードを見る限り、佐祐理様の仰るとおり三回のイベントが順に発生しているのであれば最終的には「あいうえお」の候補が表示されるかと思うのです。
    また、「あいうえお」の決定後にTextBoxに「あ」しか残らない理由も不明です。

    UIスレッドでgoogleへ候補をダウンロードしに行くのも問題でしょう。別スレッドで実行すべきです。



    はい、trapemiya様からご助言いただいたとおりSystem.Threading.Timerあたりを使用して一定期間内にTextChangeが発生しなかった時だけ別スレッドでAPI呼び出しを行うように改修してみようと思います。

    期待されているのはIME確定後ではなく、文字入力最中、例えば変換候補が出ている最中にもサジェストしたいのではありませんか? そうなってくると、TextBox.TextChangedイベントではなく、IMEから変換最中の文字列を取得せざるを得ないのでは? と思います。更には、変換中にサジェスト候補を選択したらIME側は変換の中断も必要ですよね?



    はい、仰るとおりです。
    「・入力が決定されないと候補が表示されない」がその要望になります。
    これについてはtrapemiya様からもご助言いただいておりますが、IME側との連携が必須になってくるかと思います。


    ここまでいただいたご回答をまとめさせていただきます。

    ・文字入力からAPI呼び出しが動作するまでの時間を指定できない
    →System.Threading.Timerなどを使って自分で実装しましょう。

    ・一文字ずつしか入力できない
    依然不明です。

    ・入力が決定されないと候補が表示されない
    →IMEと連携しましょう。

    ・GoogleサジェストAPIから取得した候補が全て表示されない
    対応方法が不明です。


    引き続き情報などございましたらお寄せいただけたら幸いです。
    よろしくお願いします。
    2012年2月29日 7:52
  •  以前、「IME で入力中の文字列を知りたい」の様な質問があった気がしたので、上の検索ボックスで「ime」を検索しました。結果から、ImmSetCompositionString を見つけました。しかし、今回は「取りたい」ので、「設定したい」は当てはまりません。そこで左の関数一覧を見ると、ImmGetCompositionString がありました。こいつをキーにもう一度検索すると、「テキストボックスで漢字名を入力した際にルビを振る方法について」が見つかりました。ここに、trapemiyaさんが書かれたコードがあるので、これを利用して作ってみることにします。新しい Windows Form アプリケーションでこれだけ実行し(いや、これだけだと実行できないけど)、動作の確認を行ってください。その他、佐佑理さんが指摘されている内容も考慮の上、実装してください。一文字ずつしか入力できないのは、数秒おいてが実装できれば解決すると思いますよ。

    
    using System;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    
    namespace Practice11
    {
        public partial class UserControl1 : TextBox
        {
            public UserControl1()
            {
                InitializeComponent();
            }
    
            [DllImport("Imm32.dll", CharSet = CharSet.Auto)]
            public static extern int ImmGetContext(IntPtr hWnd);
            [DllImport("Imm32.dll")]
            public static extern int ImmGetCompositionString(
                int hIMC,
                int dwIndex,
                IntPtr lpBuf,
                int dwBufLen
                );
            [DllImport("Imm32.dll", CharSet = CharSet.Auto)]
            public static extern bool ImmReleaseContext(IntPtr hWnd, int hIMC);
    
            // これは、WndProc で m.Msg を表示し、文字入力後に発生しているメッセージを総当たりした。
            const int WM_IME_NOTIFY = 0x0282;
            // これは、ヘッダファイルから適当にあたりをつけた。
            const int GCS_COMPSTR   = 0x0008;
    
            protected override void WndProc(ref Message m)
            {
                if (m.Msg == WM_IME_NOTIFY)
                {
                    if (((int)m.LParam & GCS_COMPSTR) > 0)
                    {
                        IntPtr buffer = IntPtr.Zero;
                        int Imc = ImmGetContext(this.Handle);
                        try
                        {
                            int bytes = ImmGetCompositionString(Imc, GCS_COMPSTR, IntPtr.Zero, 0);
                            buffer = Marshal.AllocHGlobal(bytes);
                            ImmGetCompositionString(Imc, GCS_COMPSTR, buffer, bytes);
                            byte[] arrByte = new Byte[bytes];
                            Marshal.Copy(buffer, arrByte, 0, bytes);
                            string str = Encoding.GetEncoding(932).GetString(arrByte);
    
                            // ここを、適切に変更する
                            System.Diagnostics.Debug.WriteLine(str);
                        }
                        finally
                        {
                            ImmReleaseContext(this.Handle, Imc);
                            Marshal.FreeHGlobal(buffer);
                        }
    
                    }
                }
                base.WndProc(ref m);
            }
        }
    }
    

    Jitta@わんくま同盟

    • 回答としてマーク 山本春海 2012年4月12日 8:13
    2012年2月29日 13:55
  • Jitta様

    ご投稿をありがとうございます。
    今気づきました。。。
    IMEの件については追々試させていただきます。

    trapemiya様、佐祐理様からご助言いただいた下記について対応しておりました。
    ・System.Threading.Timerなどを使ってAPI呼び出しが行われるまでの時間を制御
    ・API呼び出しを別スレッドで実行

    ソースを掲載させていただきます。


    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.Web;
    using System.Net;
    using System.Xml.Linq;
    using System.Xml;
    using System.Threading;
    namespace WindowsFormsApplication1
    {
        /// <summary>
        /// Googleサジェスト対応のTextBox
        /// </summary>
        public class TextBoxSuggest : TextBox
        {
            /// <summary>
            /// 入力からサジェスト機能が動作するまでの時間(ミリ秒)
            /// </summary>
            protected int completioninterval = 1000;
            /// <summary>
            /// タイマー
            /// </summary>
            protected System.Threading.Timer timer = null;
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public TextBoxSuggest()
            {
                this.TextChanged += new EventHandler(TextBoxSuggest_TextChanged);
                this.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;
                this.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource;
            }
            
            /// <summary>
            /// テキストが変更された時
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void TextBoxSuggest_TextChanged(object sender, EventArgs e)
            {
                if (timer == null)
                {
                    timer = new System.Threading.Timer(
                        new TimerCallback(createsuggest),
                        ((TextBox)sender).Text,
                        completioninterval,
                        completioninterval
                        );
                }
                else
                {
                    timer.Change(completioninterval, completioninterval);
                }
            }
            /// <summary>
            /// サジェスト候補を作成
            /// </summary>
            /// <param name="query"></param>
            protected void createsuggest(object query)
            {
                timer.Change(Timeout.Infinite, Timeout.Infinite);// このタイミングでいいのか・・・?
                AutoCompleteStringCollection ac = new AutoCompleteStringCollection();
                ac.AddRange(request(this.Text).ToArray());
                Invoke((Action)delegate() 
                {
                    this.AutoCompleteCustomSource = ac;
                }
                );
            }
            /// <summary>
            /// GoogleのAPIにリクエスト
            /// </summary>
            /// <param name="query"></param>
            /// <returns></returns>
            protected List<string> request(string query)
            {
                List<string> ret = new List<string>();
                try
                {
                    if (!string.IsNullOrEmpty(query))
                    {
                        XDocument xdoc = null;
                        string strxml = string.Empty;
                        string apiurl =
                            "http://www.google.com/complete/search?hl=ja&output=toolbar&q=" +
                            HttpUtility.UrlEncode(query);
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(apiurl);
                        IWebProxy proxy = req.Proxy;
                        if (proxy != null)
                        {
                            proxy.Credentials = CredentialCache.DefaultCredentials;
                        }
                        using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
                        {
                            strxml = (new System.IO.StreamReader(res.GetResponseStream(), Encoding.Default)).ReadToEnd();
                        }
                        xdoc = XDocument.Parse(strxml);
                        ret =
                            (
                            from
                                c
                            in
                                xdoc
                                .Descendants("CompleteSuggestion")
                            select
                                (c.Element("suggestion") != null && c.Element("suggestion").Attribute("data") != null ? c.Element("suggestion").Attribute("data").Value : string.Empty)
                            ).ToList<string>();
                    }
                }
                catch
                {
                    ret.Clear();
                }
                return ret;
            }
        }
    }



    requestメソッドについては変更はありません。
    requestメソッドを呼び出しサジェストの候補をAutoCompleteCustomSourceにセットするcreatesuggestメソッドを新規に追加しました。
    TextBoxSuggest_TextChangedの中ではTimerを使用し、一定期間内にテキスト変更が行われなかった場合別スレッドでcreatesuggestメソッドを実行するよう処理しています。

    一定期間内(上記ソースでは1秒)にテキスト変更が行われなかった場合、別スレッドでAPI呼び出し&サジェスト候補作成
    この処理自体は、甘い部分はあるかもしれませんが、望んだ通りの動きを見せているようです。

    ただし、上記コードではUI部分で思った結果が得られていません。
    上記ソースでは文字列を入力した時に候補が表示されません。
    逆に文字を削除した時に候補が表示されます。しかし、1秒ほどするとその候補が消えます。
    しかもこの時に1秒ほど表示される候補は削除前の文字列の候補が表示されています。

    Googleから取得できる「a」の候補は下記になります。
    amazon
    ameba
    au
    ana
    akb48
    アメブロ
    apple
    朝日新聞

    アスクル

    Googleから取得できる「ab」の候補は下記になります。
    abcマート
    abc
    アバクロ
    アバター
    アバスト
    阿部寛
    阿部真央
    阿部サダヲ
    アベイル
    アボカド

    「ab」と入力しても何も候補が表示されず、その後「b」を消すと候補が約1秒だけ表示されます。
    その候補は上記「a」のものではなく「ab」の候補の内容になっています。
    (実際には「・GoogleサジェストAPIから取得した候補が全て表示されない」の問題が解決されていないため「ab」の候補内から2個だけ表示されている状態ですが)

    これは下記流れによるものと思われます。
    TextBoxで候補が表示されるのは、AutoCompleteCustomSourceが設定された時ではなくテキストが変更された時のようです。
    そのため、はじめに「ab」と入力された(テキスト変更が行われた)際にはAutoCompleteCustomSourceが空のため候補が表示されません。
    その入力から1秒後にAPI呼び出しが実行されAutoCompleteCustomSourceに「ab」の候補がセットされます。しかしこのタイミングでは候補は表示されません。
    その後、「b」を消す(テキスト変更が行われた)とAutoCompleteCustomSourceには「ab」の候補が入っているためそれが表示されます。
    しかし、その1秒後に再度API呼び出しが実行されAutoCompleteCustomSourceに「a」の候補がセットされます。このタイミングで表示されている「ab」の候補が消えます。

    自分で書きながらわかり易い説明が行えていないと思っています・・・
    申し訳ないです。。。

    以上のことから、AutoCompleteCustomSourceに設定されている候補を表示するタイミングがPG側から制御できないとこの問題は解決できないと思われます。
    ですがPG側から候補を表示させるトリガーが不明な状況です。
    (AutoCompleteCustomSourceが設定されている状態でOnTextChangedを呼んでみましたが何も起きませんでした)

    やればやるほど問題が出てくる状態ですが・・・
    何かご助言などいただけたら幸いです。

    よろしくお願いいたします。
    2012年3月1日 7:16
  • Jitta様

    一文字ずつしか入力できないのは、数秒おいてが実装できれば解決すると思いますよ


    はい、前レスの「別スレッド&数秒おいて実行」版のソースコードで「一文字ずつしか入力できない」の問題は確かに解決いたしました。
    どうもありがとうございます。
    2012年3月1日 7:39
  • Jitta様

    一文字ずつしか入力できないのは、数秒おいてが実装できれば解決すると思いますよ


    はい、前レスの「別スレッド&数秒おいて実行」版のソースコードで「一文字ずつしか入力できない」の問題は確かに解決いたしました。
    どうもありがとうございます。

     TextChanged イベントの中でウェブ サービスへの問い合わせをやっているので、そこで時間がかかっている、つまり TextChanged イベントが完了せず、データがおかしくなっているのではないかと予想しました。

     TextChanged イベントの終わりで提案ウィンドウが表示されるなら(最初のコードで、入力に合わせて Autocomplete の内容が変わっているなら、そのようになっていると思われます)、もう一度 TextChanged イベントを発生させるという方法が考えられます。が、これ、面倒なんですよね、カーソルの位置を制御するのが。

     


    Jitta@わんくま同盟

    2012年3月1日 12:44
  • Jitta様

    お世話になります。

    TextChanged イベントの終わりで提案ウィンドウが表示されるなら(最初のコードで、入力に合わせて Autocomplete の内容が変わっているなら、そのようになっていると思われます)、もう一度 TextChanged イベントを発生させるという方法が考えられます。が、これ、面倒なんですよね、カーソルの位置を制御するのが。


    はい、最初のコードで入力に合わせてAutocompleteの内容が変わっています。
    しかしPG側からTextChangedを呼び出しても提案ウィンドウ(と言うのですね)は表示されませんでした。


    テスト用にTextBoxSuggestを下記のように修正して、PG側からもう一度TextChangedを呼び出してみました。
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using System.Windows.Forms;
    using System.Web;
    using System.Net;
    using System.Xml.Linq;
    using System.Xml;
    using System.Threading;
    
    namespace WindowsFormsApplication1
    {
        /// <summary>
        /// Googleサジェスト対応のTextBox
        /// TextChange再呼出のテスト用
        /// 関係しないソースは全て削除している
        /// </summary>
        public class TextBoxSuggest : TextBox
        {
    
            public void test()
            {
                this.OnTextChanged(new EventArgs());
            }
    
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public TextBoxSuggest()
            {
                this.TextChanged += new EventHandler(TextBoxSuggest_TextChanged);
                this.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;
                this.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource;
    
                // テストのため、あらかじめAutoCompleteCustomSourceを作成しておく
                AutoCompleteStringCollection ac = new AutoCompleteStringCollection();
                ac.Add("aaa");
                ac.Add("aab");
                ac.Add("abb");
                ac.Add("bbb");
                this.AutoCompleteCustomSource = ac;
            }
            /// <summary>
            /// テキストが変更された時
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void TextBoxSuggest_TextChanged(object sender, EventArgs e)
            {
                // test()が呼び出されるとここも通過するか?
            }
    
        }
    }
    


    Formにはボタンも設置されていてい、そのクリックイベントが下記のようになっています。
            private void button1_Click(object sender, EventArgs e)
            {
                textBoxSuggest1.Focus();
                textBoxSuggest1.test();
            }
    



    この状態で実行しTextBoxSuggestに「a」と入力してあげると提案ウィンドウが開き下記アイテムが表示されます。
    aaa
    aab
    abb
    (「bbb」は表示されません。)

    この状態でEscキーでも押して一度提案ウィンドウを閉じます。
    その後ボタンをクリックします。
    しかし、提案ウィンドウは表示されません。

    デバッグを行ってみると、ボタンクリック後にTextBoxSuggest_TextChangedは呼び出されています。

    提案ウィンドウの表示トリガーがいまいちわかりません。。。
    てっきりTextChanged後に表示されるものと思っていたのですが。

    何かお気付きの点などあればご助言いただけたらと思います。


    2012年3月2日 3:31
  • 内容からして、IAutoComplete を使用して提案型の補完機能を提供したいということでしょうか。

    既にご自身でも確認されていますが、IAutoComplete は入力された語を含む単語を補完する機能なので、a を入力したら a が含まれている候補を絞り込むのが目的です。a から連想される語を候補としてあげることはできません。

    また、ソースは全ての候補を格納していることを前提にしているので、入力内容にあわせえて切り替えるのではなく、入力されうるすべての候補を事前に格納しておくことが想定されています。

    Windows Vista/7 では、IAutoComplete2 のオプション機能で ACO_NOPREFIXFILTERING を on にすれば、入力内容にかかわらず、常に補完ソースのすべての値を表示する(絞り込まないで、探すだけになる)ようになるので、こちらを使うと a という入力から「朝日新聞」をドロップダウンさせることができるようになります。(プロパティからは指定できないので、COM や API のメソッドを直接呼び出して設定する必要があります)

    入力内容にあわせて補完リストを変更するために、IEnumString を実装することになると思いますが、最初に書いているようにすべての入力に対する補完候補を事前に持っていることが前提の設計なので、入力にあわせて再列挙してくれるかどうかがポイントですね。たぶんしないと思いますが、IEnumString を実装して確認してみれば結果はすぐにでると思います。


    http://msdn.microsoft.com/en-us/library/windows/desktop/bb776292(v=vs.85).aspx
    2012年3月2日 4:02
  • K. Takaoka様

    ご投稿をありがとうございます。

    内容からして、IAutoComplete を使用して提案型の補完機能を提供したいということでしょうか。


    色々調べた結果、AutoCompleteCustomSourceを使用する方法ではなくご助言いただいたIAutoComplete2とIEnumStringを使用する方法で対応するのがベストなようです。
    現在、これらを使った方法で調査・検討を進めております。
    これについては目途が立ち次第こちらにあらためて結果を掲載させていただこうと思っております。

    ちょっと話がそれるのですが、別の点で気になることが出てきています。
    GoogleサジェストのAPIを使用していいのか。という点です。

    http://www.google.com/complete/search?hl=ja&output=toolbar&q=[query]
    このURLで取得した結果を何かしらの方法で使用したアプリやシステムを配布していいのか。
    この点が気になってきました。

    そもそも上記URLはGoogleサジェストの結果を取得するために公開されているAPIという位置づけと判断していいものでしょうか?
    私の調べた限りではGoogleのオフィシャルなドキュメントとして上記URLの説明がされているようなものが見当たらないようなのです。

    上記URLでGoogleサジェストの結果を得てそれを利用することは、いわゆる「スクレイピング」に該当してしまうのではないかと懸念しております。

    もしこの点について何か知見・ノウハウをお持ちの方がいらしたらご意見などお聞かせいただけたら幸いです。



    現時点の問題点とその対応方法についてあらためてまとめさせていただきます。

    1.文字入力からAPI呼び出しが動作するまでの時間を指定できない
    →System.Threading.Timerなどを使って自分で実装しましょう。

    2.一文字ずつしか入力できない
    →「1.」に対応することで解決する

    3.入力が決定されないと候補が表示されない
    →IMEと連携しましょう。優先度低め

    4.GoogleサジェストAPIから取得した候補が全て表示されない
    →IAutoComplete2&IEnumStringを使用しましょう。現在検討中。

    5.そもそもGoogleサジェストの結果って使っていいの?
    →不明
    2012年3月3日 2:52
  • Google の利用規約に、Google が用意したインターフェース以外でアクセスする行為は禁止とされています。
    ドキュメントなどで明示されていない URL を直接呼び出す行為は、抵触するのでは?
    明確な判断がほしいのであれば、Google に問い合わせるべきでしょう。

    私見ですが、何らかのソフトウェアに向けて用意されたものが出回っているという感じに見えます。
    私がこの情報を得たとして、外部に公開する方向に持って行くのであれば、事前に問い合わせは必須だと考えています。後から規約違反で閉め出された場合、ユーザーから不満の声が上がるでしょうから。(最初からないのと、途中で使えなくなるのは印象がかなり違う)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。

    2012年3月3日 3:23
    モデレータ
  • Azulean様
    ご投稿を恐れ入ります。

    明確な判断がほしいのであれば、Google に問い合わせるべきでしょう。


    仰る通りかと思います。
    この件は正確な判断が難しいだろうことと、これ自体の対応に予想以上の面倒なコスト(おもに時間かな・・・)がかかりそうなことから、とりあえずは「GoogleサジェストのAPIを使用することは利用規約に反する行為」という前提で進めていこうと思います。

    本スレッドはタイトルの通り「Googleサジェスト"風"のTextBoxを実装する」ことが目的ですので、GoogleサジェストのAPIは使用せずに可能な範囲で様々な結果等を引き続きこちらでご報告させていただこうと思います。

    2012年3月3日 4:47
  •  「TextChanged イベントが発生する」のと、「OnTextChanged メソッドを呼び出す」のは違います。後者では、前者が通る処理をすべて通るとはいえません。

     「面倒なんですよね、カーソルの位置を制御するのが。」と書いたのは、TextBox.Text を操作して TextChanged イベントを発生させるからです。Text プロパティを変更すると、カーソルの位置が変わります。同じものを代入しても、イベントは発生しません。追加や後ろから削除は良いのですが、文字列の真ん中を変更すると、カーソル位置が変わってしまいます。なので、面倒なんですよね、カーソルの位置を制御するのが。SelectIndex だったか何かで現在のカーソル位置が取れるので、それを保存して、Text に空白を代入した後、それまでに設定されていた内容をもう一度設定し、カーソル位置を復元する、という手順になります。


    Jitta@わんくま同盟

    2012年3月5日 14:35
  • http://www.google.com/complete/search?hl=ja&output=toolbar&q=[query]
    このURLで取得した結果を何かしらの方法で使用したアプリやシステムを配布していいのか。
    この点が気になってきました。

    現在は非公開のAPIで使用許可がないようですね。Google Codes でも追加要望としてかなりの数の vote が上がっていると思います。

    代替手段として、Yahoo! API に同じものがあるので、そちらを選択しているという人もいるようでした。(しかし、Y!のほうは候補があまりよろしくないので Google の機能も公開してくれ、と)

    ところで、理解されているのかと思っていますが、AutoCompleteMode プロパティやAutoCompleteCustomSource プロパティは、IAutoComplete を .NET コントロールが実装したものです。(IAutoComplete2 は実装されていません) ですので、IAutoComplete2 のオプション設定以外については、ほぼ既存の機能で対応できますよ。

    2012年3月8日 3:59