1. 程式人生 > >利用C#實現工作列通知視窗

利用C#實現工作列通知視窗

首先新增引用:然後新增如下程式碼:
            想必大部分網友都使用過QQ、MSN等聊天程式,它們的介面都相當華麗,尤其是當網友上線以及訊息提示時會有一個浮動的窗體從螢幕的右下方緩慢升起,淨化裝置既美觀又人性化,作為程式設計師在享受的同時我們也不禁要問:這到底是怎麼實現的呢?本文就利用Visual Studio .Net C# 2005以及.Net框架繪圖技術來實現這種工作列通知視窗。 

  簡介

  QQ和MSN的工作列通知視窗很人性化,淨化工程它可以在不丟失主窗體焦點的前提下顯示一個具備面板Skin的通知窗體,當它顯示一段時間後會自動消失,所以使用者根本不用幹預它。這樣的通知窗體和一般的具備標題欄、系統圖標和按鈕的窗體沒有太大的區別,窗體表面其實就是畫上去的一張點陣圖而已,而窗體的浮動則會複雜一點,我們會用到.Net框架的雙重緩衝區繪圖技術(參見作者編譯文章
"Windows 窗體的.Net框架繪圖技術")來保證移動窗體時所顯示的內容平滑且不閃爍,以及使用P/Invoke平臺呼叫進行對Win32API函式的呼叫來完成不獲得焦點的窗體顯示和非標題欄窗體拖動。兩種點陣圖的面板執行時的介面如下:

 


  背景知識

  通知視窗就是將一般的窗體附加上一層面板,這裡所謂的面板就是一張點陣圖圖片,該點陣圖圖片通過窗體的OnPaintbackground事件被繪製到窗體表面,在附加點陣圖之前需要調整窗體的可視屬性,由於繪製操作是針對於窗體客戶區域的,所謂客戶區域就是指窗體標題欄下方以及窗體邊框以內的所有區域,所以需要將窗體的邊框和外觀屬性 FormBorderStyle調整為:None,這樣所繪製的影象就會填充整個窗體。

  首先,我們會用到Region物件,Region物件可以精確的描繪出任意形狀的輪廓範圍,通過一個位圖影象建立Region物件後再將其傳遞給窗體的Region屬性就可以使窗體按照Region所定義的輪廓顯示出來。作為面板使用的點陣圖檔案可以通過任何影象編輯軟體諸如:Photeshop來建立和編輯,只是注意一點,需要將圖片的背景色調成特定顏色以便程式繪製時將其清除,我們在這裡使用的背景色為粉紅色。為了能夠讓Region物件按照影象中感興趣的內容邊框來建立窗體,我們還需要使用GraphicsPath類將影象輪廓按照一定路徑標註下來,稍後便按照該路徑建立Region物件。

  然後通過窗體的繪圖事件將點陣圖的內容顯示在窗體表面,我們沒有直接使用OnPaintbackground事件而是過載了該方法,這樣做的好處就是一些低層的繪製操作還繼續交由.Net框架執行時來處理,我們只考慮實際需要的繪製操作即可。在OnPaintbackground方法中我們啟用了雙重緩衝區繪圖技術,所謂該技術就是指先在記憶體中的一塊畫布上把將要顯示的影象顯示出來或進行處理,等到操作完成再將該畫布上所顯示的影象放置到窗體表面,這樣的機制可以非常有效的降低閃爍的出現,使影象顯示更加平滑。通知窗體從螢幕的右下方進行升起停留一段時間後再慢慢回落,這裡需要用到返回螢幕區域的大小範圍的.Net框架方法 Screen.GetWorkingArea(WorkAreaRectangle),通過一定演算法計算出通知窗體顯示前的初始位置。最後,我們將要顯示的文字按照一定格式和Rectangle物件所指定的區域範圍繪製到窗體表面。通知窗體的關閉操作是通過設定一個區域,當用戶用滑鼠單擊時檢測單擊座標是否在該區域內,若在區域內就可以執行隱藏通知窗體的程式碼。

  我們注意了,當QQ和MSN的通知視窗顯示時其主窗體的焦點沒有丟失,也就是說程式沒有將自身的焦點轉移到顯示的通知窗體上。經過測試,我們無論怎麼樣呼叫.Net框架提供的窗體顯示例程譬如:Form.Show都無法保證主窗體的焦點不丟失,在VC環境下我們可以使用Win32API的ShowWindows函式來完成複雜的窗體顯示操作,但是.Net框架根本沒有提供類似的方法,那麼我們能否通過.Net框架呼叫該API函式來顯示窗體呢?幸好.Net框架提供了P
/Invoke平臺呼叫,利用平臺呼叫這種服務,託管程式碼就可以呼叫在動態連結庫中實現的非託管函式,並可以封送其引數,我們可以輕鬆的顯示但不獲得焦點的窗體。程式中用到的Windows API以及常量的定義都儲存在WinUser.h標頭檔案中,其對應的動態連結庫檔案就是user32.dll,使用.Net框架提供的DllImportAttribute類對匯入的函式進行定義,然後就可以非常方便的在程式中呼叫該函數了。

  由於我們將通知窗體的標題欄隱藏了,所以對窗體拖動操作還需要我們自己動手進行處理。本文介紹瞭如何更加高效的進行拖動窗體操作,有些網友在對於非標題欄拖動窗體程式設計時偏向組合使用滑鼠事件來進行,這樣做的本質沒有任何不妥,但是頻繁的事件響應和處理反而使程式效能有所降低。我們將繼續使用Win32API的底層處理方法來解決該問題,就是向窗體傳送標題欄被單擊的訊息,模擬實際的拖動操作。

  我們會通過2個計時器來完成窗體的顯示、停留和隱藏,通過設定速度變數可以改變視窗顯示和隱藏的速度。

  程式實現

  啟動Visual Studio .Net 
2005,建立C# Windows 窗體應用程式,將解決方案命名為TaskbarForm,包含的專案名也為TaskbarForm,首先建立程式的主窗體Form1,在上面新增兩個Button控制元件,一個用於顯示通知窗體,另一個則終止程式。然後在解決方案管理器中右擊專案,單擊"新增 - Windows 窗體",我們把新建立的窗體命名為TaskbarForm。

  在類TaskbarForm定義的下方,我們建立用於顯示的字串和其顏色的變數,再定義幾個Rectangle物件的變數用於放置標題、提示內容以及可以拖動窗體的區域和關閉按鈕的區域。然後,我們需要儲存窗體在浮動時的高度以便計算移動後的新高度,intervalValue變數用來確定窗體顯示和隱藏的速度。進行平臺呼叫時我們需要提前定義好常量的值用來傳遞給函式,WM_NCLBUTTONDOWN和HT_CAPTION常量用於拖動窗體,他們的值都儲存在WinUser.h標頭檔案中,所對應的動態連結庫名為:user32.dll。我們用到的Win32API為:SendMessage、ReleaseCapture和ShowWindow,通過使用DllImportAttribute可以匯入相應的函式並在程式中重新進行定義,如下:



[DllImportAttribute(
"user32.dll")]
publicstaticexternint SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); 
//傳送訊息//winuser.h 中有函式原型定義
[DllImportAttribute("user32.dll")]
publicstaticexternbool ReleaseCapture(); //釋放滑鼠捕捉winuser.h
[DllImportAttribute("user32.dll")] //winuser.h
privatestaticextern Boolean ShowWindow(IntPtr hWnd, Int32 nCmdShow);

  SendMessage向訊息迴圈傳送標題欄被按下的訊息來模擬窗體的拖動,ShowWindow用來將特定控制代碼的窗體顯示出來,注意第二個引數nCmdShow,它表示窗體應該怎樣顯示出來,而我們需要窗體不獲得焦點顯示出來,SW_SHOWNOACTIVATE可以滿足我們要求,繼續在WinUser.h檔案中搜索找到該常量對應的值為4,於是我們就可以這樣呼叫來顯示窗體了:



ShowWindow(
this.Handle, 4);

  我們建立了一個自定義函式ShowForm用來封裝上面的ShowWindow用來是顯示窗體,同時傳遞了所用到的幾個Rectangle矩形區域物件,最後呼叫ShowWindows函式將窗體顯示出來,程式碼片段如下:



publicvoid ShowForm(string ftitletext, string fcontenttext, Rectangle fRegionofFormTitle, Rectangle fRegionofFormTitlebar, Rectangle fRegionofFormContent, Rectangle fRegionofCloseBtn)
{
 titleText 
= ftitletext;
 contentText 
= fcontenttext;
 WorkAreaRectangle 
= Screen.GetWorkingArea(WorkAreaRectangle);
 
this.Top = WorkAreaRectangle.Height +this.Height;
 FormBorderStyle 
= FormBorderStyle.None;
 WindowState 
= FormWindowState.Normal;
 
this.SetBounds(WorkAreaRectangle.Width -this.Width, WorkAreaRectangle.Height - currentTop, this.Width,  this.Height);
 CurrentState 
=1;
 timer1.Enabled 
=true;
 TitleRectangle 
= fRegionofFormTitle;
 TitlebarRectangle 
= fRegionofFormTitlebar;
 ContentRectangle 
= fRegionofFormContent;
 CloseBtnRectangle 
= fRegionofCloseBtn;
 ShowWindow(
this.Handle, 4); //#define SW_SHOWNOACTIVATE 4
}


  CurrentState變量表示窗體的狀態是顯示中、停留中還是隱藏中,兩個計時器根據窗體不同狀態對窗體的位置進行更改,我們會使用SetBounds來執行該操作:



this.SetBounds(WorkAreaRectangle.Width -this.Width, WorkAreaRectangle.Height - currentTop, this.Width, this.Height);

  當窗體需要升起時將窗體的Top屬性值不斷減少,而窗體回落時將Top屬性值增加並超過螢幕的高度窗體就消失了,雖然原理很簡單但仍需精確控制。 

 

 

  SetBackgroundBitmap函式首先將窗體背景影象儲存到BackgroundBitmap變數中,然後根據該點陣圖影象輪廓和透明色建立Region,BitmapToRegion就用於完成Bitmap到Region的轉換,程式再將這個Region付值給窗體的Region屬性以完成不規則窗體的建立。



publicvoid SetBackgroundBitmap(Image image, Color transparencyColor)
{
 BackgroundBitmap 
=new Bitmap(image);
 Width 
= BackgroundBitmap.Width;
 Height 
= BackgroundBitmap.Height;
 Region 
= BitmapToRegion(BackgroundBitmap, transparencyColor);
}


public Region BitmapToRegion(Bitmap bitmap, Color transparencyColor)
{
 
if (bitmap ==null)
  
thrownew ArgumentNullException("Bitmap""Bitmap cannot be null!");

 
int height = bitmap.Height;
 
int width = bitmap.Width;
 GraphicsPath path 
=new GraphicsPath();
 
for (int j =0; j < height; j++)
  
for (int i =0; i < width; i++)
  
{
   
if (bitmap.GetPixel(i, j) == transparencyColor)
    
continue;
   
int x0 = i;
   
while ((i < width) && (bitmap.GetPixel(i, j) != transparencyColor))
    i
++;
   path.AddRectangle(
new Rectangle(x0, j, i - x0, 1));
  }

  Region region 
=new Region(path);
  path.Dispose();
  
return region;
}

 
  通知窗體背景以及文字的繪製在過載的OnPaintBackground方法中完成,而且利用了雙重緩衝區技術來進行繪製操作,程式碼如下:



protectedoverridevoid OnPaintBackground(PaintEventArgs e)
{
 Graphics grfx 
= e.Graphics;
 grfx.PageUnit 
= GraphicsUnit.Pixel; 
 Graphics offScreenGraphics;
 Bitmap offscreenBitmap;
 offscreenBitmap 
=new Bitmap(BackgroundBitmap.Width, BackgroundBitmap.Height);
 offScreenGraphics 
= Graphics.FromImage(offscreenBitmap);
 
if (BackgroundBitmap !=null)
 
{
  offScreenGraphics.DrawImage(BackgroundBitmap, 
00, BackgroundBitmap.Width, BackgroundBitmap.Height);
 }

 DrawText(offScreenGraphics);
 grfx.DrawImage(offscreenBitmap, 
00);
}


  上述程式碼首先返回窗體繪製表面的Graphics並儲存在變數grfx中,然後建立一個記憶體Graphics物件offScreenGraphics和記憶體點陣圖物件offscreenBitmap,將記憶體點陣圖物件的引用付值給offScreenGraphics,這樣所有對offScreenGraphics的繪製操作也都同時作用於offscreenBitmap,這時就將需要繪製到通知窗體表面的背景影象BackgroundBitmap繪製到記憶體的Graphics物件上,DrawText函式根據需要顯示文字的大小和範圍呼叫Graphics.DrawString將文字顯示在窗體的特定區域。最後,呼叫Graphics.DrawImage將記憶體中已經繪製完成的影象顯示到通知窗體表面。

  我們還需要捕獲窗體的滑鼠操作,有三個操作在這裡進行,
1、處理拖動窗體操作,2、處理通知窗體的關閉操作,3、內容區域的單擊操作。三個操作都需要檢測滑鼠的當前位置與每個Rectangle區域的包含關係,只要單擊落在特定區域我們就進行相應的處理,程式碼如下: 



privatevoid TaskbarForm_MouseDown(object sender, MouseEventArgs e)
{
 
if (e.Button == MouseButtons.Left)
 
{
  
if (TitlebarRectangle.Contains(e.Location)) //單擊標題欄時拖動
{
   ReleaseCapture(); 
//釋放滑鼠捕捉
   SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0); //傳送左鍵點選的訊息至該窗體(標題欄)
  }

  
if (CloseBtnRectangle.Contains(e.Location)) //單擊Close按鈕關閉 
{
   
this.Hide();
   currentTop 
=1;
  }

  
if (ContentRectangle.Contains(e.Location )) //單擊內容區域
{
   System.Diagnostics.Process.Start(
"http://www.Rithia.com");
  }

 }

}


  結論

  該程式可以很好的進行通知窗體的顯示、停留和隱藏操作,並且具備簡單的換膚機制,在利用了雙重緩衝區繪圖技術後,可以保證窗體的繪製平滑且沒有閃爍。