天天看點

訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單

          在此前一篇文章中已經給出了一個訪範例。這裡在以前的基礎上進行進一步的美化工作。(本範例是屬于windows 應用程式範疇,即傳統的桌面應用程式,開發環境是VC6.0 + Windows SDK)。

          (1)給自定義繪制的視窗增加圓角。

          系統中的任何視窗在系統的視角邏輯上都被認為是矩形,然而我們能看到很多應用程式會使用一些具有特殊邊緣形狀的各種視窗(例如qqmusic的浮出視窗)。在這裡我是通過 SetWindowRgn 函數來給一個視窗設定區域。視窗的區域類似PS中的蒙版,位于區域外部的窗體部分将不可見。一個複雜區域實際上是一些互相沒有重疊的矩形組成的集合。效果下圖所示。

          對于複雜區域的設定我們可以通過一副Bitmap(這裡使用的就是浮出視窗的背景圖)來得到。即這裡規定了洋紅色(RGB255,0,255)為一個特殊顔色,是以我們就可以給一個視窗劃定成圓角輪廓,使他更美觀,這裡邊框一并也都在背景圖來繪制完成了。

          使用 SetWindowRgn 函數時需要注意的是,傳遞給該函數的HRGN,一旦調用了函數以後,該RGN的維護權就從應用程式移交給了作業系統,也就是說應用程式不應該再對該RGN句柄做任何其他操作了(例如DeleteObject等)。這一點對于我們需要緩存一些複雜區域是非常關鍵的,例如如果一個視窗需要經常切換區域,而這個區域的生成成本又很高(要周遊一個位圖的所有像素),這時我們很自然就會想把得到的區域緩存起來。這裡務必注意的是,不能把緩存的RGN的句柄直接傳遞給 SetWindowRgn 函數,否則這個RGN緩存可能已經會系統回收導緻此後将不再有效。正确的做法是,每次更新RGN時,建立一個緩存RGN的副本拷貝傳遞給該函數。

訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單

          (2)一個典型的提供三态滑鼠回報的Button。

          一個button對滑鼠提供三種不同外觀回報,分别是,普通(滑鼠在遠處),懸停(滑鼠懸浮在按鈕上方),按下(滑鼠在按鈕上按下)。這裡我引用了qqmusic中的資源圖檔,如圖2所示。在這裡要注意的是,這種回報具有以下幾個特點:

          a。滑鼠在按鈕上按下,保持按下狀态,移動到遠處擡起,按鈕事件不會被觸發。這時按鈕對按下期間的滑鼠移動提供“普通”和“按下”回報。

          b。滑鼠在遠處按下,保持按下狀态移動到按鈕上方,按鈕為懸停态。這時如果滑鼠在按鈕上方擡起,同樣不會觸發按鈕事件。

          從上面兩點非常細節化的設計上(我個人感覺這樣的好處主要是為了給使用者一種非常細微的可以“撤銷”點選按鈕的方式,當然這樣也就會相應增加了實作的難度)可以看出,按鈕事件被觸發的條件是:滑鼠的按下點和擡起點都位于按鈕的“有效點選區域”内。滑鼠的按下點和擡起點我們可以在收到WM_LBUTTONDOWN和WM_LBUTTONUP消息時得到并更新,同時我們也必須記錄滑鼠按鈕的目前狀态(使用一個BOOL變量來記錄滑鼠是否處于按下狀态)。

          還要注意的時,當有多個按鈕時,滑鼠在其中某個按鈕上方按下以後,正常做法時,應用程式内部有一個“追蹤對象”就會被設定為該按鈕,此後滑鼠在按下期間的移動,隻有“被追蹤”的對象對其提供回報,其他按鈕将不會做任何反應。這個方式非常類似應用程式之間的滑鼠捕獲。在這個小例子裡由于隻有一個關閉按鈕,是以為了簡便,就沒有這樣去做處理。

          另一點,對這種回報我們還必須注意到,我們對按鈕狀态的切換主要是根據MoveMove消息來完成的,而滑鼠事件的另外一個特點是:MouseMove消息是斷續的,而不是我們感觀感受到的“連續”狀态。也就是說,當滑鼠用非常快的速度移動時,我們可能會得到一些間距很大的離散點。而且我們沒有捕獲滑鼠,而系統隻會把消息傳遞給滑鼠“下方”的視窗。是以這樣我們上面的按鈕三态反應将很可能因為滑鼠快速的離開視窗,而使視窗不知道滑鼠實際已經離開按鈕(由于MouseMove的離散性),進而使按鈕停留在一個“中間狀态”,而無法複原到Normal狀态。這時我們就必須要求系統通知我們一個特殊的滑鼠消息,即WM_MOUSELEAVE。我想大概處于性能考慮,微軟預設情況下是不給視窗發送MouseLeave通知的,是以我們如果要求系統通知我們WM_MOUSELEAVE消息,就必須調用 TrackMouseEvent 函數,要求系統為我們送達我們需要的消息。這個函數的使用時需要注意的是,Track的有效期是自調用函數開始到我們要求的消息到來之前的期間一直有效,而一旦我們要求的消息已經産生并送達,就算完成了一次Track過程(此後的這種消息,系統就不再發送通知)。是以我們應該把這個函數的調用放到MouseMove消息中進行,即一旦滑鼠進入我們的視窗“領空”,我們就不厭其煩的請求系統發送WM_MOUSELEAVE消息。這樣我們就能保證不管滑鼠移動的多麼快,我們都會正确得到通知,進而保證按鈕狀态可順利恢複到Normal狀态。

          (3)自定義繪制菜單。

          在這部分,我參考到了BCMenu的源代碼(BC是作者姓名的首字母)。但BCMenu是用MFC類庫的,而我的應用程式中是沒有采用MFC類庫的,是以我就自己寫了不攜帶MFC的C語言版本的一個簡要的自定義繪制菜單,即例子代碼中Menu.h和Menu.C。

          如果應用程式想要自定義菜單的繪制,那麼就相應的要為此必須承擔一些責任,例如“測量”和“繪制”MenuItem。菜單的複雜情況決定這并不是一個簡單輕松的任務。例如MenuItem有Checked,Unchecked, Grayed, Selected, 等不同狀态。細節不再具體叙述了。在例子中的菜單一些配色參考了Ms office 2003和qqmusic。

訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單

          (4)其他附加功能:

          a。在右下角可顯示一個URL,當滑鼠點選時,會使用預設浏覽器打開顯示的網址。預設浏覽器的路徑是通過查詢系統資料庫資訊獲得的。

          b。對标題的自動截斷,如果從某個漢字的兩個bytes中間截斷,将會造成顯示“?”(因為系統不知道這裡是什麼字元)。這種情況一般是在對于char*這種ASCII碼式的C字元串(即MultiByteString)才會出現,因為字元的位元組數為1到2不等。如果是Unicode(即WideCharString),因為字元位元組數固定,則不存在這種問題。

          是以,如果要做的更好,我們應該在截斷時嚴格的從漢字邊界處截斷(而不能從某個漢字的中間切斷)。對于char*這種ASCII碼式的C字元串,漢字和ASCII字元的差別就是 判斷起始位元組是否大于等于0xa1 ,如果是,則這裡是漢字,否則就是普通的ASCII字元(英文字元)。但如果是漢字,我們還是不知道這是屬于某漢字的前半還是後半部分(尤其是當連續漢字時)。這時要擷取漢字邊界,我們必須從字元串開始位置向後一直掃描,直到掃描到這裡我們才能準确的把漢字邊界劃出來。即遇到ascii碼就向後移動一格,如果遇到漢字就向後移動兩格繼續判斷。直到判斷到和要截斷位置距離小于2個bytes為止。這時隻有兩種可能,如果距離為0,則說明恰好位于字元邊界處,可以直接截斷字元串。若距離為1byte,則說明處于某個漢字中間,可以把階段位置前移(切掉目前漢字),或後移(保留目前漢字)一個byte再做截斷。

          另外必須注意的是,如果參數是char*,必須先把char強轉為unsigned char類型才能和0xa1進行比較(否則漢字字元的byte可能被當作負數看待!)。

          下面是能夠正确從漢字邊界處把标題自動截斷的相應代碼:

訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單
訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單

Code_标題自動截斷

//設定NotifyWnd标題

void SetNotifyTitle(LPCTSTR title)

{

    unsigned int i;

    unsigned int maxlength = sizeof(m_NotifyTitle)-1;

    //需要注意的是,用char*是無法和0xa1比較的,因為會被當作負數!!!

    unsigned char *s=(unsigned char*)title;

    //置空(重要)

    memset(m_NotifyTitle, 0 ,maxlength+1);

    //判斷是否要截斷标題,注意要判斷漢字!

    if(strlen(title) > maxlength)

    {

        //strncpy(m_NotifyTitle, title, maxlength - 5);

        //為了保證不把漢字從中間階段,是以我們一個一個字元複制過去

        for(i=0; i < maxlength - 3; i++)

        {

            if(s[i]>= 0xa1)

            {

                //是漢字,連續複制2個位元組

                m_NotifyTitle[i]=title[i];

                i++;

            }

            else

                //是英文,複制一個位元組即可

        }

        //無需null結束字元(因為此前已經把m_NotifyTitle清零了。

        strcat(m_NotifyTitle, "

訪MSN浮出式視窗(2)--視窗區域和自定義繪制菜單

"); //追加省略号

    }

    else

        strcpy(m_NotifyTitle,title);

}

          最後,是範例代碼的下載下傳連結:

繼續閱讀