作為程序員在享受的同時我們也不禁要問這到底是怎麼實現的呢?本文就利用Visual Studio Net C# 以及Net框架繪圖技術來實現這種任務欄通知窗口
簡介
QQ和MSN的任務欄通知窗口很人性化它可以在不丟失主窗體焦點的前提下顯示一個具備皮膚Skin的通知窗體當它顯示一段時間後會自動消失所以用戶根本不用干預它
這樣的通知窗體和一般的具備標題欄系統圖標和按鈕的窗體沒有太大的區別窗體表面其實就是畫上去的一張位圖而已而窗體的浮動則會復雜一點我們會用到Net框架的雙重緩沖區繪圖技術(參見作者編譯文章Windows窗體的Net框架繪圖技術)來保證移動窗體時所顯示的內容平滑且不閃爍以及使用P/Invoke平台調用進行對WinAPI函數的調用來完成不獲得焦點的窗體顯示和非標題欄窗體拖動兩種位圖的皮膚運行時的界面如下
背景知識
通知窗口就是將一般的窗體附加上一層皮膚這裡所謂的皮膚就是一張位圖圖片該位圖圖片通過窗體的OnPaintbackground事件被繪制到窗體表面在附加位圖之前需要調整窗體的可視屬性由於繪制操作是針對於窗體客戶區域的所謂客戶區域就是指窗體標題欄下方以及窗體邊框以內的所有區域所以需要將窗體的邊框和外觀屬性FormBorderStyle調整為None這樣所繪制的圖像就會填充整個窗體
首先我們會用到Region對象Region對象可以精確的描繪出任意形狀的輪廓范圍通過一個位圖圖像創建Region對象後再將其傳遞給窗體的Region屬性就可以使窗體按照Region所定義的輪廓顯示出來作為皮膚使用的位圖文件可以通過任何圖像編輯軟件諸如Photeshop來創建和編輯只是注意一點需要將圖片的背景色調成特定顏色以便程序繪制時將其清除我們在這裡使用的背景色為粉紅色為了能夠讓Region對象按照圖像中感興趣的內容邊框來創建窗體我們還需要使用GraphicsPath類將圖像輪廓按照一定路徑標注下來稍後便按照該路徑創建Region對象
然後通過窗體的繪圖事件將位圖的內容顯示在窗體表面我們沒有直接使用OnPaintbackground事件而是重載了該方法這樣做的好處就是一些低層的繪制操作還繼續交由Net框架運行時來處理我們只考慮實際需要的繪制操作即可在OnPaintbackground方法中我們啟用了雙重緩沖區繪圖技術所謂該技術就是指先在內存中的一塊畫布上把將要顯示的圖像顯示出來或進行處理等到操作完成再將該畫布上所顯示的圖像放置到窗體表面這樣的機制可以非常有效的降低閃爍的出現使圖像顯示更加平滑
通知窗體從屏幕的右下方進行升起停留一段時間後再慢慢回落這裡需要用到返回屏幕區域的大小范圍的Net框架方法ScreenGetWorkingArea(WorkAreaRectangle)通過一定算法計算出通知窗體顯示前的初始位置
最後我們將要顯示的文本按照一定格式和Rectangle對象所指定的區域范圍繪制到窗體表面通知窗體的關閉操作是通過設定一個區域當用戶用鼠標單擊時檢測單擊坐標是否在該區域內若在區域內就可以執行隱藏通知窗體的代碼
我們注意了當QQ和MSN的通知窗口顯示時其主窗體的焦點沒有丟失也就是說程序沒有將自身的焦點轉移到顯示的通知窗體上經過測試我們無論怎麼樣調用Net框架提供的窗體顯示例程譬如FormShow都無法保證主窗體的焦點不丟失在VC環境下我們可以使用WinAPI的 ShowWindows函數來完成復雜的窗體顯示操作但是Net框架根本沒有提供類似的方法那麼我們能否通過Net框架調用該API函數來顯示窗體呢?
幸好Net框架提供了P/Invoke平台調用利用平台調用這種服務托管代碼就可以調用在動態鏈接庫中實現的非托管函數並可以封送其參數我們可以輕松的顯示但不獲得焦點的窗體程序中用到的Windows API以及常量的定義都保存在WinUserh頭文件中其對應的動態鏈接庫文件就是userdll使用Net框架提供的 DllImportAttribute類對導入的函數進行定義然後就可以非常方便的在程序中調用該函數了
由於我們將通知窗體的標題欄隱藏了所以對窗體拖動操作還需要我們自己動手進行處理本文介紹了如何更加高效的進行拖動窗體操作有些網友在對於非標題欄拖動窗體編程時偏向組合使用鼠標事件來進行這樣做的本質沒有任何不妥但是頻繁的事件響應和處理反而使程序性能有所降低我們將繼續使用 WinAPI的底層處理方法來解決該問題就是向窗體發送標題欄被單擊的消息模擬實際的拖動操作
我們會通過個計時器來完成窗體的顯示停留和隱藏通過設置速度變量可以改變窗口顯示和隱藏的速度
[DllImportAttribute(userdll)] public static extern int SendMessage(IntPtr hWnd int Msg int wParam int lParam)//發送消息//winuserh 中有函數原型定義[DllImportAttribute(userdll)] public static extern bool ReleaseCapture() //釋放鼠標捕捉winuserh [DllImportAttribute(userdll)] //winuserh private static extern Boolean ShowWindow(IntPtr hWnd Int nCmdShow)SendMessage向消息循環發送標題欄被按下的消息來模擬窗體的拖動ShowWindow用來將特定句柄的窗體顯示出來注意第二個參數 nCmdShow它表示窗體應該怎樣顯示出來而我們需要窗體不獲得焦點顯示出來SW_SHOWNOACTIVATE可以滿足我們要求繼續在 WinUserh文件中搜索找到該常量對應的值為於是我們就可以這樣調用來顯示窗體了
ShowWindow(thisHandle )我們創建了一個自定義函數ShowForm用來封裝上面的ShowWindow用來是顯示窗體同時傳遞了所用到的幾個Rectangle矩形區域對象最後調用ShowWindows函數將窗體顯示出來代碼片段如下
public void ShowForm(string ftitletext string fcontenttext Rectangle fRegionofFormTitleRectangle fRegionofFormTitlebar Rectangle fRegionofFormContent Rectangle fRegionofCloseBtn)
{ titleText = ftitletextcontentText = fcontenttextWorkAreaRectangle = ScreenGetWorkingArea(WorkAreaRectangle)thisTop = WorkAreaRectangleHeight + thisHeightFormBorderStyle = FormBorderStyleNoneWindowState = FormWindowStateNormalthisSetBounds(WorkAreaRectangleWidth thisWidthWorkAreaRectangleHeight currentTop thisWidth thisHeight)CurrentState = timerEnabled = trueTitleRectangle = fRegionofFormTitleTitlebarRectangle = fRegionofFormTitlebarContentRectangle = fRegionofFormContentCloseBtnRectangle = fRegionofCloseBtnShowWindow(thisHandle ) //#define SW_SHOWNOACTIVATE } CurrentState變量表示窗體的狀態是顯示中停留中還是隱藏中兩個計時器根據窗體不同狀態對窗體的位置進行更改我們會使用SetBounds來執行該操作
thisSetBounds(WorkAreaRectangleWidth thisWidth WorkAreaRectangleHeight currentTop thisWidth thisHeight)當窗體需要升起時將窗體的Top屬性值不斷減少而窗體回落時將Top屬性值增加並超過屏幕的高度窗體就消失了雖然原理很簡單但仍需精確控制
SetBackgroundBitmap函數首先將窗體背景圖像保存到BackgroundBitmap變量中然後根據該位圖圖像輪廓和透明色創建RegionBitmapToRegion就用於完成Bitmap到Region的轉換程序再將這個Region付值給窗體的Region屬性以完成不規則窗體的創建
public void SetBackgroundBitmap(Image image Color transparencyColor)
{ BackgroundBitmap = new Bitmap(image)Width = BackgroundBitmapWidthHeight = BackgroundBitmapHeightRegion = BitmapToRegion(BackgroundBitmap transparencyColor)} public Region BitmapToRegion(Bitmap bitmap Color transparencyColor)
{ if (bitmap == null)
throw new ArgumentNullException(Bitmap Bitmap cannot be null!)int height = bitmapHeightint width = bitmapWidthGraphicsPath path = new GraphicsPath()for (int j = j < height j++)
for (int i = i < width i++)
{ if (bitmapGetPixel(i j) == transparencyColor)
continueint x = iwhile ((i < width) && (bitmapGetPixel(i j) != transparencyColor))
i++pathAddRectangle(new Rectangle(x j i x ))} Region region = new Region(path)pathDispose()return region}通知窗體背景以及文字的繪制在重載的OnPaintBackground方法中完成而且利用了雙重緩沖區技術來進行繪制操作代碼如下
protected override void OnPaintBackground(PaintEventArgs e)
{ Graphics grfx = eGraphicsgrfxPageUnit = GraphicsUnitPixelGraphics offScreenGraphicsBitmap offscreenBitmapffscreenBitmap = new Bitmap(BackgroundBitmapWidth BackgroundBitmapHeight)ffScreenGraphics = GraphicsFromImage(offscreenBitmap)if (BackgroundBitmap != null)
{ offScreenGraphicsDrawImage(BackgroundBitmap BackgroundBitmapWidth BackgroundBitmapHeight)} DrawText(offScreenGraphics)grfxDrawImage(offscreenBitmap )}上述代碼首先返回窗體繪制表面的Graphics並保存在變量grfx中然後創建一個內存Graphics對象 offScreenGraphics和內存位圖對象offscreenBitmap將內存位圖對象的引用付值給offScreenGraphics這樣所有對offScreenGraphics的繪制操作也都同時作用於offscreenBitmap這時就將需要繪制到通知窗體表面的背景圖像 BackgroundBitmap繪制到內存的Graphics對象上DrawText函數根據需要顯示文字的大小和范圍調用 GraphicsDrawString將文字顯示在窗體的特定區域最後調用GraphicsDrawImage將內存中已經繪制完成的圖像顯示到通知窗體表面
我們還需要捕獲窗體的鼠標操作有三個操作在這裡進行處理拖動窗體操作處理通知窗體的關閉操作內容區域的單擊操作三個操作都需要檢測鼠標的當前位置與每個Rectangle區域的包含關系只要單擊落在特定區域我們就進行相應的處理代碼如下
private void TaskbarForm_MouseDown(object sender MouseEventArgs e)
{ if (eButton == MouseButtonsLeft)
{ if (TitlebarRectangleContains(eLocation)) //單擊標題欄時拖動{ ReleaseCapture() //釋放鼠標捕捉SendMessage(Handle WM_NCLBUTTONDOWN HT_CAPTION ) //發送左鍵點擊的//消息至該窗體(標題欄)
} if (CloseBtnRectangleContains(eLocation)) //單擊Close按鈕關閉{ thisHide()currentTop = } if (ContentRectangleContains(eLocation )) //單擊內容區域{ SystemDiagnosticsProcessStart() } }結論
該程序可以很好的進行通知窗體的顯示停留和隱藏操作並且具備簡單的換膚機制在利用了雙重緩沖區繪圖技術後可以保證窗體的繪制平滑且沒有閃爍
From:http://tw.wingwit.com/Article/program/net/201311/11965.html