2013年9月11日 星期三

[轉貼] 跟我做WinForm開發(2)-後台邏輯操作

出處:http://www.cnblogs.com/kongyiyun/archive/2012/01/08/2316345.html



上一篇中,我簡單了介紹了實現自定義UI的步驟和其中一些需要注意的點;詳見:跟我做WinForm開發(1)-自定義UI,下面,我就繼續完成上篇沒完成的邏輯操作;

獲取聲音

這是一個發音器,聲音的來源是Google,打開Google翻譯,輸入一段英文,並點擊發音,Google很快就讀取了我所輸入的句子,打開HttpWatch,發現,實際上每次發音,都會把輸入的句子做一次UrlEncode,然後發往Google服務器,最後返回一個Mp3的流;這個URL如下http://translate.google.cn/translate_tts?ie=UTF-8&q=hello%20world%2C2012&tl=en&prev=input;從上面我們應該可以看到去參數就是你要發音的內容,而tl就是該語言的簡寫;那麼我們需要做的,就是修改q獲得我們想要的MP3流;PS:在後面的嘗試當中,我發現Google做了限制,只允許長度為100;超出100則無返回結果,這個100是Length;而不是所佔字節長度,所以,中文在這裡更佔優勢;
好了;既然目標已確定,那就開始吧;那有什麼辦法能讓我拿到這個MP3流呢?答案不言而喻,就是HttpWebRequest;在這裡,我新建了一個叫HttpHelper的類,它主要用於做簡單的Http Get請求;
internal static class HttpHelper
{
    /// <summary>
    /// 發起請求,用於GET.
    /// </summary>
    /// <param name="url">The URL.</param>
    internal static void SendRequest(string url, WebRequestCallBack callBack)
    {
        Log.Info("Send Request");
        try
        {
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
            request.KeepAlive = false;
            request.Method = "GET";
            CallbackParam cp = new CallbackParam
            {
                Request = request,
                CallBack = callBack
            };
            Log.Info("Begin get Responsen.");
            request.BeginGetResponse(BeginGetResponse, cp);
        }
        catch (Exception ex)
        {
            Log.Info("Error:" + ex.Message);
            callBack(null, HttpStatusCode.Continue, WebExceptionStatus.UnknownError, ex);
        }
    }

    static void BeginGetResponse(IAsyncResult ar)
    {
        Log.Info("Get Response");
        CallbackParam cp = ar.AsyncState as CallbackParam;
        try
        {
            using (HttpWebResponse response = (HttpWebResponse)cp.Request.EndGetResponse(ar))
            {
                using (var responseStream = response.GetResponseStream())
                {
                    MemoryStream ms = new MemoryStream();
                    byte[] buffer = new byte[1024];
                    int byteReader;
                    do
                    {
                        byteReader = responseStream.Read(buffer, 0, buffer.Length);
                        ms.Write(buffer, 0, byteReader);
                    }
                    while (byteReader > 0);

                    ms.Flush();
                    cp.CallBack(ms, response.StatusCode, WebExceptionStatus.Success, null);
                }
            }
        }
        catch (WebException ex)
        {
            Log.Info("Error:" + ex.Message);
            HttpWebResponse response = (HttpWebResponse)ex.Response;
            cp.CallBack(null, response.StatusCode, ex.Status, ex);
        }
        catch (Exception ex)
        {
            Log.Info("Error:" + ex.Message);
            cp.CallBack(null, HttpStatusCode.Continue, WebExceptionStatus.UnknownError, ex);
        }
    }
}
從上面的代碼看到,首先,我創建了一個HttpWebRequest對象,並將其請求方法設置為GET;隨後,開始了異步請求;當獲取到
服務器響應的時候,便將相應流讀出來,發送給回調方法,這裡為什麼要用異步?一個是避免主線程阻塞,導致UI掛起;另外就是這面編碼看起來更有Feel吧! :-)~~
現在就可以直接獲取MP3流了吧!等等;還不行!應為我們還沒講輸入的字符串UrlEncode,那怎麼辦?寫唄!
public class SpeakTextHandler
{

    public static string GetSpeakerUrl(string sourceText)
    {
        var config = ConfigManger.GetConfig();
        return string.Format(config.SpeakerUrl, GetEncodeText(sourceText), GetTextLanguage(config, sourceText));
    }

    private static string GetEncodeText(string source)
    {
        if (string.IsNullOrEmpty(source)) return string.Empty;
        return HttpUtility.UrlPathEncode(source);
    }

    private static string GetTextLanguage(Config config, string source)
    {

        foreach (var language in config.Languages)
        {
            if (Regex.Match(source, language.Unicode).Success)
                return language.Name;
        }
        return config.DefaultLanguage.Name;
    }
}
這裡我創建了一個SpeakerTextHandler的類,它主要工作就是將傳入的字符串UrlEncode,並獲取配置,拼接出相應的Url;到這
裡,獲取聲音就大功告成了!

播放聲音

拿到聲音之後需要做什麼?必然就是將其播放出來;由於沒弄過這方面的東西(平時都是些Asp.net);好吧,就Google吧;網上給出了好幾個解決方案;
A:使用SoundPlayer;這個很明顯就不行,有使用經驗的童鞋應該知道,SoundPlayer只支持wav格式的播放,雖然它能支持傳入Stream流參數,但若是傳入MP3流,還是報異常;
B:WindowsMediaPlay;但是這個必須是讀取文件;先不說是否支持MP3,單單是每次都需要先把流存儲到本地再讀取;我這懶人就無法忍受了;
C:利用DX庫來操作;這個淡淡看解決方案就很復雜;雖然可控性可能比較強;但這復雜度。。懶人望而生畏!:-(
難道就沒其他解決方案了嗎?幾經波折,終於在StackOverflow中找到了一個可行方案;
其中使用的是一個叫NAudio的開源組件,那麼,它的確是可以解決我現在的窘境;照著StackOverflow上的代碼來寫,的確是可以播放出軟件了,但是隨後關閉軟件的時候,都會出現一個錯誤的斷言,跟蹤NAudio的實現,發現是流沒釋放;囧;最終幾次嘗試;終於把這個錯誤斷言去掉了;
if (stream != null && ex == null)
{
    stream.Position = 0;
    var mp3Reader = new Mp3FileReader(stream);
    var pcmStream = WaveFormatConversionStream.CreatePcmStream(mp3Reader);
    using (WaveStream blockAlignedStream = new BlockAlignReductionStream(pcmStream))
    {
        using (WaveOut waveOut = new WaveOut(WaveCallbackInfo.FunctionCallback()))
        {
            waveOut.Init(blockAlignedStream);
            waveOut.PlaybackStopped += (sender, e) =>
            {
                waveOut.Stop();
            };
            waveOut.Play();
            while (waveOut.PlaybackState == PlaybackState.Playing)
            {
                System.Threading.Thread.Sleep(100);
            }
            waveOut.Dispose();
        }
    }
該實現在HttpHelper的回調當中(更多代碼請看後面放出的下載);

快捷鍵支持

除了僅僅能發音,那還需要支持快捷鍵放大/縮小;或者是快捷鍵發音等;那還等什麼?Come On!實際上,對這方面有經驗的同學應該就能很自然的想到利用的就是「鉤子」,當然,這個鉤子的概念和我們平時編寫代碼時所使用的鉤子這個概念有所區別,例如,Asp.net中控件/Page中有很多事件,OnLoad,OnCompleted等等。我們寫代碼的時候也可能會寫一些空實現,讓子類來做實現;這就是編程概念上的「鉤子」,而這裡的「鉤子」,是指在觸發系統一些事件的時候,也把我們所依附上的方法也執行了;其中的實現主要還是依靠與Win32API;
public delegate void HotkeyEventHandler(int hotKeyID);
public class Hotkey : System.Windows.Forms.IMessageFilter
{
    Hashtable keyIDs = new Hashtable();
    IntPtr hWnd;

    public event HotkeyEventHandler OnHotkey;

    [DllImport("user32.dll")]
    public static extern UInt32 RegisterHotKey(IntPtr hWnd, UInt32 id, UInt32 fsModifiers, UInt32 vk);

    [DllImport("user32.dll")]
    public static extern UInt32 UnregisterHotKey(IntPtr hWnd, UInt32 id);

    [DllImport("kernel32.dll")]
    public static extern UInt32 GlobalAddAtom(String lpString);

    [DllImport("kernel32.dll")]
    public static extern UInt32 GlobalDeleteAtom(UInt32 nAtom);

    public Hotkey(IntPtr hWnd)
    {
        this.hWnd = hWnd;
        Application.AddMessageFilter(this);
    }

    public int RegisterHotkey(Keys Key, KeyFlags keyflags)
    {
        UInt32 hotkeyid = GlobalAddAtom(System.Guid.NewGuid().ToString());
        RegisterHotKey((IntPtr)hWnd, hotkeyid, (UInt32)keyflags, (UInt32)Key);
        keyIDs.Add(hotkeyid, hotkeyid);
        return (int)hotkeyid;
    }

    public void UnregisterHotkeys()
    {
        Application.RemoveMessageFilter(this);
        foreach (UInt32 key in keyIDs.Values)
        {
            UnregisterHotKey(hWnd, key);
            GlobalDeleteAtom(key);
        }
    }

    public bool PreFilterMessage(ref System.Windows.Forms.Message m)
    {
        if (m.Msg == 0x312)
        {
            if (OnHotkey != null)
            {
                foreach (UInt32 key in keyIDs.Values)
                {
                    if ((UInt32)m.WParam == key)
                    {
                        OnHotkey((int)m.WParam);
                        return true;
                    }
                }
            }
        }
        return false;
    }
}

[Flags]
public enum KeyFlags
{
    Alt = 0x1,
    Ctrl = 0x2,
    Shift = 0x4,
    Win = 0x8
}
調用方法很簡單;
Hotkey hotkey = new Hotkey(handle);
int speakHotkey = hotkey.RegisterHotkey(System.Windows.Forms.Keys.Q, KeyFlags.Ctrl);
hotkey.OnHotkey += new HotkeyEventHandler(it =>
{
    if (it == speakHotkey)
    {
        SendCtrlC(Win32.GetForegroundWindow());
        Thread.Sleep(500);
        Speaker.Speak(Clipboard.GetText());
    }
});

屏幕取詞

相信大部分同學都有使用過翻譯軟件,其中的屏幕取詞不可謂不是一大殺器,如果你想翻譯一個單詞都必須要先復制,然後在打開翻譯軟件,粘貼,這樣的話效率未免也太低了,對於用戶體驗也不好;於是,我便想著自己實現這方面的功能;Google許久,得出主要的實現方式如下:
其中可行的方法就是利用金山詞霸的dll,可惜,最終嘗試都失敗了!不過,其中單選記事本,編輯器中的文字成功,但瀏覽器/其他軟件讀詞失敗;看來,屏幕取詞,不涉及到底層,單單用C#來實現還是很有難度;那我就換個思路吧,也只能通過選中文字,按下快捷鍵,先復制到剪貼板中,再將其讀取出來;經過幾番努力,最後可行方案如下:
 public class HotKeyManager
 {
     public static void RegistSelectedSectionHotKey(IntPtr handle)
     {
         Hotkey hotkey = new Hotkey(handle);
         int speakHotkey = hotkey.RegisterHotkey(System.Windows.Forms.Keys.Q, KeyFlags.Ctrl);
         hotkey.OnHotkey += new HotkeyEventHandler(it =>
         {
             if (it == speakHotkey)
             {
                 SendCtrlC(Win32.GetForegroundWindow());
                 Thread.Sleep(500);
                 Speaker.Speak(Clipboard.GetText());
             }
         });
     }


     private static void SendCtrlC(IntPtr hWnd)
     {
         Win32.SetForegroundWindow(hWnd);
         Win32.keybd_event(0x11, 0, 0, 0);
         Win32.keybd_event(67, 0, 0, 0);
         Win32.keybd_event(0x11, 0, 2, 0);
         Win32.keybd_event(67, 0, 2, 0);
     }
 }
我新建了一個HotKeyManager的類,裡面的RegistSelectedSectionHotKey方法注冊了熱鍵,事件先通過
Win32API發送復制指令,線程阻塞500毫秒(將文字復制到剪貼板中需要一定的延遲時間);然後獲取剪切板的文字就大功告成了!

尾聲

項目雖小,卻多有趣味;其中更是應用到了一些我從來沒接觸過的東西;也學到了不少東西;故寫下備忘;也給需要這方面資料的童鞋一個幫助;總是如此,寫下來時總覺無什麼可寫;但其中的收獲和感言卻不少;只有親自動手才能有所收獲;以後還需多多寫!多多益善~~
源碼下載:Speaker.rar

沒有留言:

張貼留言