一、靈感來源:html
LZ是純宅男,一天從早上8:00起一直要呆在電腦旁到晚上12:00左右吧~平時也沒人來閒聊幾句,刷空間暑假也沒啥動態,聽音樂吧...~有些確實很差聽,因而就不得不打斷手頭的工做去點擊下一曲或是找個好聽的歌來聽...可是,[移動手鎖定鼠標-->移動鼠標關閉當前頁面選擇音樂軟件頁面-->選擇合適的音樂-->恢復原來的界面] 這一過程也會煩人很多,若是說軟件的設計要在用戶體驗上作足功夫,感受這一點是軟件設計人員很難管住的方面,畢竟操做系統也就這樣安排的嘛(固然,有些機智的開發人員加了幾個熱鍵,確實方便了很多!)。因而我想能不能設計一個軟件能儘可能少打斷咱們正常的工做簡單操做去觸發下一曲~正則表達式
二、需求分析:算法
三、解決方案windows
根據上面分析咱們須要這些條件:數組
結合我現有設備,作出以下方案:ide
PS:這裏根據手在超聲波範圍內停留的時間來分出3種信號:函數
四、做品提早展現及相關介紹:post
哈哈,秒懂啦吧!圖中那個像望遠鏡的東西就是超聲波測距模塊,它的前面輻射狀的空間(我設置爲40cm)就是有效範圍,那個黑色的像蜈蚣的東西就是單片機(就至關於電腦裏的CPU),插在USB裏面的不用介紹就是USB轉TTL啦!主要就是負責採集傳感器信號而後將距離信息經過USB發送給電腦。最終達到達到的效果是:你的手只要在區域內揮一下,就能切歌啦!手停長一點時間就能暫停啦!這個玩法沒試過吧,哈哈!測試
下面這個圖就是基於C#的電腦端軟件,其主要功能就是鏈接串口進行數據接收、數據處理、以及查找音樂盒的窗口、計算該點擊的按鈕位置、發出點擊消息、在不一樣窗口中切換(由於要實現少打擾當前活動的目的)。這裏爲了測試方便因此加了3個功能按鈕:上一曲、暫停、下一曲,經過點擊這些按鈕能實現控制酷我音樂盒歌曲的切換,而後右邊加了個下拉框用來枚舉當前可用串口,LINK按鈕就是鏈接該串口的觸發按鈕。下面一個文本顯示區是用來顯示串口傳過來的距離的數據的(便於調試哈~)動畫
五、C#軟件部分技術詳解
該部分要用到不少Windows API,主要功能就是查找窗口句柄、控制窗口顯示、計算窗口位置、聚焦窗口、窗口切換....算是把窗口有關的經常使用API都用上啦~此外,還用到了鼠標光標位置設定、鼠標點擊消息發送最終達到模擬鼠標點擊事件。固然,串口通訊絕對不能少滴!
5.一、C#串口通訊
5.1.一、獲取當前可用串口列表
1 //Get all port list for selection 2 //得到全部的端口列表,並顯示在列表內 3 PortList.Items.Clear(); 4 string[] Ports = SerialPort.GetPortNames(); 5 6 for (int i = 0; i < Ports.Length; i++) 7 { 8 string s = Ports[i].ToUpper(); 9 Regex reg = new Regex("[^COM\\d]", RegexOptions.IgnoreCase | RegexOptions.Multiline);//正則表達式 10 s = reg.Replace(s, ""); 11 12 PortList.Items.Add(s); 13 } 14 if (Ports.Length >1) PortList.SelectedIndex = 1;
5.1.二、串口鏈接按鈕事件
1 private void btn_link_Click(object sender, EventArgs e) 2 { 3 if (!Connection.IsOpen) 4 { 5 //Start 6 Status = "正在鏈接..."; 7 Connection = new SerialPort(); 8 btn_link.Enabled = false; 9 Connection.PortName = PortList.SelectedItem.ToString(); 10 Connection.Open(); 11 Connection.ReadTimeout = 10000; 12 Connection.DataReceived += new SerialDataReceivedEventHandler(PortDataReceived); 13 Status = "鏈接成功"; 14 } 15 }
PS:總體很好理解就是把下拉框選中的串口號鏈接上,這裏第12行比較重要,它調用SerialDataReceivedEventHandler(Func Name)來定義一個數據接收函數的句柄,這裏PortDataReceived你能夠隨便寫,可是接下來你要寫對應的實現函數:(這裏說句柄比較難理解,你就理解成一個函數,綁定串口的函數,一旦串口有數據發動過來就執行這個函數....)
1 //接收串口數據 2 private int num=0; //障礙物進入範圍的時間 3 private bool enter=false; //是否有障礙物進入 4 private int signal=0; //對每次進入範圍的時間分段造成控制信號 5 private void PortDataReceived(object o, SerialDataReceivedEventArgs e) 6 { 7 int length = 1; 8 byte[] data = new byte[length]; 9 Connection.Read(data, 0, length); 10 for (int i = 0; i < length; i++) 11 { 12 ReceivedData = string.Format("{0}",data[i]); 13 } 14 15 //數據濾波轉換爲控制信號 16 if (data[0] != 136 && !enter){ //當有障礙物進入時,傳過來數據不是136而且是第一個 17 enter = true; 18 num = 1; 19 }else if (data[0] == 136 && enter){ //當障礙物離開時,傳過來數據變爲136且是第一個 20 enter = false; 21 if (num > 1 && num < 6){ 22 signal = 1; 23 }else if (num > 5 && num < 10){ 24 signal = 2; 25 }else if (num > 9){ 26 signal = 3; 27 } 28 num = 0; 29 }else if (data[0] != 136 && data[0] >= 0 && enter){ 30 num++; 31 } 32 }
PS:這就是串口數據接收函數實現,先別看其餘內容,由於裏面涉及濾波算法和控制信號生成的算法,只要看第7~13行的代碼核心部分就是第9行從緩衝區讀取串口數據放到data[]數組中,這樣串口數據就放在data[]中啦!怎麼處理是下面的事啦~
5.1.三、重量級功能函數:
1 /// <summary> 2 /// 模擬鼠標點擊函數 3 /// </summary> 4 /// <param name="n_control_type">0是上一曲,1是暫停,2是下一曲</param> 5 public void func(int n_control_type) 6 { 7 //bool isVisabled; //窗口原來狀態,隱藏仍是顯示 8 IntPtr hCurWin = GetForegroundWindow(); //獲取當前激活窗口 9 10 IntPtr hMusic = FindWindow("kwmusicmaindlg", null); //找到窗口句柄 11 if (hMusic == null) 12 { 13 return; 14 } 15 Point pt; //獲取鼠標當前位置 16 GetCursorPos(out pt); 17 ShowWindow(hMusic,SW_SHOWNORMAL); //若是是隱藏的就讓他正常顯示出來 18 SetForegroundWindow(hMusic); //將音樂盒窗口放在最上層 19 20 RECT rect = new RECT(); //獲取窗口矩形 21 GetWindowRect(hMusic, ref rect); 22 int width = rect.Right - rect.Left; //窗口的寬度 23 int height = rect.Bottom - rect.Top; //窗口的高度 24 int x = rect.Right; //窗口的位置 25 int y = rect.Top; 26 27 int X=0,Y=0; 28 if(n_control_type==0)//座標[-20,200]:第3列表 [-120,200]:第2列表 [-220,200]第1列表 29 { //座標[-200,100]:上一曲 [-170,100]暫停 [-145,100]下一曲 30 X = x - 200; 31 Y = y + 100; 32 } 33 else if (n_control_type == 1) 34 { 35 X = x - 170; 36 Y = y + 100; 37 } 38 else 39 { 40 X = x - 145; 41 Y = y + 100; 42 } 43 44 SetCursorPos(X, Y); //移動鼠標 45 mouse_event(MOUSEEVENTF_LEFTDOWN, X * 65536 / 1024, X * 65536 / 768, 0, 0); //發送鼠標信息 46 mouse_event(MOUSEEVENTF_LEFTUP, Y * 65536 / 1024, Y * 65536 / 768, 0, 0); 47 SetCursorPos(pt.X, pt.Y); //移動鼠標回到原位置 48 49 //if (isVisabled == 24) ShowWindow(hMusic, SW_HIDE); 50 //SetParent(hMusic, this.Handle); 51 //EnableWindow((IntPtr)this.Handle, true); 52 SetWindowPos(hMusic, (IntPtr)this.Handle, x, y, width, height, SWP_NOMOVE); //使能窗口聚焦原窗口 53 SetForegroundWindow(hCurWin); //將原來窗口放在最上層 54 }
PS:這個函數負責找到酷我音樂盒的窗口(第10行)、頂層窗口切換(第18行、第52行、第53行)、鼠標位置設置(第16行、第44行、第47行)、鼠標點擊消息的生成(第45行、第46行)、點擊區域計算(第27~42行)
1 [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] 2 public static extern IntPtr GetForegroundWindow();
FindWindow("kwmusicmaindlg", null);根據窗口類名或者窗口名得到窗口句柄。PS:該如何知道某個窗口的類名或者窗口名呢?通常是用VC6.0或者是VS系列軟件的Tool-->Spy++,具體請見我寫的一篇博文,裏面有詳細介紹:http://www.cnblogs.com/zjutlitao/p/3889900.html
1 [DllImport("user32.dll", EntryPoint = "FindWindow")] 2 public static extern IntPtr FindWindow( 3 string lpClassName, 4 string lpWindowName 5 );
GetCursorPos(out pt);獲取當前鼠標的位置,保存在Point結構體內,這裏由於咱們想讓鼠標點擊一下按鈕而後回到原來的位置,因此要保存原來的位置!
1 [DllImport("user32.dll")] 2 public static extern bool GetCursorPos(out Point pt);
ShowWindow(hMusic,SW_SHOWNORMAL);根據句柄顯示窗口,這裏第二個參數是設定窗口以哪一種方式顯示的,主要有以最小化顯示、最大化顯示、正常顯示.....具體參見度娘~咱們這裏是爲了不有時候音樂盒最小化,咱們得把它打開才能觸發點擊事件有效。(我原本想用個標記來標記它原來的狀態而後在處理以後恢復音樂盒自身的狀態,可是以爲還得寫些代碼,沒時間啦,調試這個浪費了很長時間~)
1 //private readonly int SW_HIDE = 0; //隱藏 2 private readonly int SW_SHOWNORMAL = 1; //還原 3 [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] 4 private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
SetForegroundWindow(hMusic); 將活動窗口切換到句柄所指窗口,這樣鼠標點擊對應區域窗口才能接收到鼠標點擊消息!
1 [DllImport("user32.dll")] 2 private static extern bool SetForegroundWindow(IntPtr hWnd);
GetWindowRect(hMusic, ref rect); 獲取指定窗口的在桌面上的矩形座標(這樣就能根據這個值計算目標窗口的大小和位置啦:20~25行就是幹這個的)
1 [DllImport("user32.dll")] 2 [return: MarshalAs(UnmanagedType.Bool)] 3 static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); 4 5 [StructLayout(LayoutKind.Sequential)] 6 public struct RECT 7 { 8 public int Left; //最左座標 9 public int Top; //最上座標 10 public int Right; //最右座標 11 public int Bottom; //最下座標 12 }
SetCursorPos(X, Y); 設置鼠標光標位置(X,Y)
1 [DllImport("user32.dll", EntryPoint = "SetCursorPos")] 2 private static extern int SetCursorPos(int x, int y);
mouse_event(MOUSEEVENTF_LEFTDOWN, X * 65536 / 1024, X * 65536 / 768, 0, 0); 發送消息函數,咱們知道windows是消息機制的,你點一下鼠標其實就是光標移到指定位置,而後向系統發送一個鼠標按動消息,這裏我仿製一個鼠標左擊時間,第45行負責在指定位置發送個鼠標左鍵按下的消息,第46行發送個對應的鼠標左鍵擡起的消息,這樣一按一擡就組成了一個點擊事件。
1 private readonly int MOUSEEVENTF_LEFTDOWN = 0x2; 2 private readonly int MOUSEEVENTF_LEFTUP = 0x4; 3 [DllImport("user32")] 4 public static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo);
SetWindowPos(hMusic, (IntPtr)this.Handle, x, y, width, height, SWP_NOMOVE); 這個函數和ShowWindow有點像,只是這個能夠設置窗口的三維顯示,爲何是三維?平面窗口還有一維是窗口的疊放順序,具體能夠問度娘~(這裏刪了這句好像也沒啥影響,當初由於沒有下面那句,因此須要這個函數將焦點放到C#軟件窗口)
1 static readonly IntPtr HWND_TOP = new IntPtr(0); 2 const UInt32 SWP_NOMOVE = 0x0002; 3 [System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowPos", SetLastError = true)] 4 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
5.1.四、時間函數TImer
往窗口裏加一個Timer控件:[長下面那個模樣,屬性設置爲Interval:100,而後給它一個消息函數,屬性中的那個閃電的標誌],C#比MFC要方便的多,MFC要本身寫這貨,有點麻煩,可是對於打基礎的童鞋仍是建議從win32學起,而後再學MFC這樣你對windows消息機制會有比較清晰的理解!嘿嘿,撤遠啦!其實這個Timer對應的消息函數就像一個會定時執行的函數同樣,你只要在裏面寫些邏輯,它會每隔必定的時間執行的。好比你想作動畫效果,讓一個小球移動,那麼小球的座標的改變的計算能夠放在這裏面寫。下面看一下個人這個函數中寫了什麼:
1 private string Status, ReceivedData; 2 private void timer1_Tick(object sender, EventArgs e) 3 { 4 StatusMessage.Text = Status; 5 StatusMessage.Text = ReceivedData; 6 //當有有效信號過來觸發控制 7 if (signal == 1) func(2);//下一曲 8 if (signal == 2) func(0);//上一曲 9 if (signal == 3) func(1);//暫 停 10 signal = 0; 11 }
PS:其實就是更新那個文本顯示區的內容和根據上面串口收來的數據進行處理而後產生的3種不一樣的控制命令,來調用func函數執行不一樣的點擊命令!
>_<:好啦,軟件部分終於說完啦(那其它3個功能按鈕直接調用func函數就行啦),其實硬件部分更多,剛纔一直沒有說那個濾波算法,及對應的命令信號signal是如何產生的....下面就要介紹啦!
六、硬件部分及濾波、信號產生算法詳解:
其實硬件部分就是CPU採集超聲波測距儀採集的距離的信息經過串口發送給電腦,電腦再對發送過來的數據進行分析,來看看是要切歌仍是暫停仍是一些干擾(這裏在硬件和圖像處理中常常會談到的名詞:濾波)。這裏只貼一下硬件部分的代碼(難點是濾波,硬件是基於stc80c52的程序,包括與測距模塊的通訊程序、串口通訊程序兩大部分,具體細節裏面有很詳細的註釋,建議若是是新手最好看看《新概念51單片機C語言教程》不錯的哦~)
1 /***********************************************************************************************************/ 2 //HC-SR04 超聲波測距模塊應用程序 3 //MCU: STC89C52/STC89C51 4 //晶振:11。0592 5 //接線:模塊TRIG接 P1.2 ECH0 接P1.1 6 //串口波特率9600 7 /***********************************************************************************************************/ 8 #include <AT89X51.H> 9 #include <intrins.h> 10 #include <STDIO.H> 11 12 #define uchar unsigned char 13 #define uint unsigned int 14 #define RX P1_1 15 #define TX P1_2 16 17 18 unsigned int time=0; 19 unsigned int timer=0; 20 unsigned char S=0,a; 21 bit flag =0,usart_flag; 22 23 24 /*-------------------------------------------- 25 USAR初始函數包括全部須要的中斷和時鐘,超聲波時鐘也在內] 26 ---------------------------------------------*/ 27 void USRT_init() 28 { 29 TMOD=0x21; //設置T1定時器工做方式2,設T0爲方式1,GATE=1; 30 SCON=0x50; 31 TH1=0xfd; //T1定時器裝初值 32 TL1=0xfd; 33 TH0=0; //超聲波測距計時器裝初始值 34 TL0=0; 35 TR1=1; //啓動T1定時器 36 TR0=1; 37 REN=1; //容許串口中斷接收、 38 ET0=1; //容許T0中斷 39 SM0=0; //設定串口工做方式 40 SM1=1; 41 EA=1; //開總中斷 42 ES=1; //開串口中斷 43 } 44 /*-------------------------------------------- 45 串口發送函數 46 ---------------------------------------------*/ 47 void SeriPushSend(unsigned send_data) 48 { 49 SBUF=send_data; 50 while(!TI); 51 TI=0; 52 } 53 /*-------------------------------------------- 54 串口中斷程序 55 ---------------------------------------------*/ 56 void ser()interrupt 4 57 { 58 RI=0; 59 a=SBUF; 60 usart_flag=1; 61 } 62 /*-------------------------------------------- 63 超聲波距離計算函數 64 ---------------------------------------------*/ 65 void Conut(void) 66 { 67 time=TH0*256+TL0; 68 TH0=0; 69 TL0=0; 70 S=(int)(time*1.87)/100; //算出來是CM 71 if(flag==1 || S>30) //超出測量或無效數據 72 { 73 flag=0; 74 SeriPushSend(0x88); 75 } 76 else 77 { 78 SeriPushSend(S); 79 } 80 } 81 /*-------------------------------------------- 82 毫秒延時函數 83 ---------------------------------------------*/ 84 void delayms(unsigned int ms) 85 { 86 unsigned char i=100,j; 87 for(;ms;ms--) 88 { 89 while(--i) 90 { 91 j=10; 92 while(--j); 93 } 94 } 95 } 96 /*-------------------------------------------- 97 超聲波測距中斷函數[計時用] 98 ---------------------------------------------*/ 99 void zd0() interrupt 1 //T0中斷用來計數器溢出,超過測距範圍 100 { 101 flag=1; //中斷溢出標誌 102 } 103 /*-------------------------------------------- 104 超聲波測距啓動函數 105 ---------------------------------------------*/ 106 void StartModule() //T1中斷用來掃描數碼管和計800MS啓動模塊 107 { 108 TX=1; //800MS 啓動一次模塊 109 _nop_(); 110 _nop_(); 111 _nop_(); 112 _nop_(); 113 _nop_(); 114 _nop_(); 115 _nop_(); 116 _nop_(); 117 _nop_(); 118 _nop_(); 119 _nop_(); 120 _nop_(); 121 _nop_(); 122 _nop_(); 123 _nop_(); 124 _nop_(); 125 _nop_(); 126 _nop_(); 127 _nop_(); 128 _nop_(); 129 _nop_(); 130 TX=0; 131 } 132 /*-------------------------------------------- 133 main函數 134 ---------------------------------------------*/ 135 void main(void) 136 { 137 USRT_init(); 138 while(1) 139 { 140 StartModule(); 141 while(!RX); //當RX爲零時等待 142 TR0=1; //開啓計數 143 while(RX); //當RX爲1計數並等待 144 TR0=0; //關閉計數 145 Conut(); //計算 146 delayms(10); //10MS 147 } 148 }
>_<:下面將重點介紹如何從距離信息轉換爲按鈕觸發消息的!
6.一、檢測手勢:
下圖是當有手進入測距區時超聲波測距儀採集到的數據,其中橫軸爲時間,縱軸爲距離單位釐米。從圖中能夠看出當沒有障礙物時距離維持在42CM處(這是我在示波器軟件中故意設置的一個閾值,硬件代碼裏也設了閾值即:超出30cm就發送距離爲0x88cm)。當手揮進對應區域時出現一個降低沿,當手離開時出現一個上升沿,當手在區域中停留的時間越長其對應跨度越大。(圖中共有4個凹槽,表示手4次揮進揮出區域,其中第3次停留時間較長)
6.二、干擾信號:
以下圖(不要管上面的圖標,當時用的時候沒修改圖表的單位和名稱,嘻嘻~)當沒有手進入區域時有時候硬件會出現干擾而產生一個很尖的降低和上升沿,其實這時並無手揮進區域,這個干擾會對結果形成影響,甚至出現錯誤的控制!!!
6.三、去除干擾:
以下圖最下面的窗口是距離-時間圖,其中第一、二、4爲手揮進測距區,第3個是一次干擾。我是這樣轉換的:將距離-時間圖轉換爲左上角的時長-時間圖,每一個波的峯值就是對應距離時間圖中跳變時間,這樣咱們就能將每次手進入或者是干擾持續的時間的值得到!(因爲干擾幾乎都是瞬間跳變,因此濾掉那個最小的第3個時長-時間波峯對應的距離-時間圖中的跳變就行啦)
6.四、時長分段產生將控制信號signal:
這裏將遮蔽時長進行分段產生3種不一樣的控制信號:[參見5.1.2串口數據接收函數的第21~27行](這裏num就是時長,可見:當時長爲2~5時產生signal爲1的信號,參看Timer部分能夠發現這個信號控制點擊下一曲;當時長在6~9的時候觸發上一曲;當時長在10以上觸發暫停)由於我常常要下一曲因此設成手一揮就執行,暫停通常操做較少就讓它時長長一點(就像筆記本電腦的關機按鈕!),加入上一曲是爲了防止失誤時能回到上面一個。!!!注意到這裏沒有把時長爲1的包含在內,這就是上面分析的結果,即所謂的濾波!消除干擾~
1 if (num > 1 && num < 6){ 2 signal = 1; 3 }else if (num > 5 && num < 10){ 4 signal = 2; 5 }else if (num > 9){ 6 signal = 3; 7 }
七、總結:
哈哈,終於寫完啦!>_<:快天亮啦~其實我原本想用腦電波來控制的,可是如今手頭有點吃緊,買不起腦電波呀~只能又一次玩廉價消費品啦~不過想一下連揮一揮手都不用的操做,是否是酷炫極啦!
PS:相關連接[僅供參考,相關API]
博主主頁(打擊盜版用@-@嘻嘻):http://www.cnblogs.com/zjutlitao/
上述工程C#代碼下載鏈接:http://pan.baidu.com/s/1hq89sHY
上述工程硬件代碼下載鏈接:http://pan.baidu.com/s/1i3IGEdn
上述工程波形分析MFC工程下載鏈接(我沒仔細註釋):http://pan.baidu.com/s/1c0w6izQ
C# 獲取當前活動窗口句柄,獲取窗口大小及位置:http://aurorax.org/372/
C# SetCursorPos用法:http://www.xuebuyuan.com/278395.html
C#調整目標窗體的位置、大小[MoveWindow]:http://www.cnblogs.com/zhuiyi/archive/2012/07/09/2583024.html
MFC 查找其餘窗口句柄 操做其餘窗口:http://www.cnblogs.com/zjutlitao/p/3614980.html
Showwindow 及參數:http://blog.csdn.net/bychentufeiyang/article/details/7164171
EnableWindow:http://baike.baidu.com/view/1080059.htm?fr=aladdin
C#多顯示器轉換的兩種方法——SetWindowPos,Screen:http://blog.csdn.net/hejialin666/article/details/6057551
SetWindowPos:http://baike.baidu.com/view/1080349.htm?fr=aladdin
[外掛2]鼠標單擊事件:http://www.cnblogs.com/zjutlitao/p/3624084.html
SetForegroundWindow:http://baike.baidu.com/view/1080341.htm?fr=aladdin