代码改变世界

跟我做WinForm开发(2)-后台逻辑操作

2012-01-08 12:32  空逸云  阅读(4918)  评论(27编辑  收藏

上一篇中,我简单了介绍了实现自定义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中找到了一个可行方案;

Play Audio from a Stream using C#

其中使用的是一个叫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许久,得出主要的实现方式如下:

[讨论]关于C#屏幕取词的技术实现
200分求[C#屏幕取词]
屏幕取词技术实现原理与关键源码
如何获取鼠标选中文本内容,请赐教
C# 如何获取鼠标选中文本内容

其中可行的方法就是利用金山词霸的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