在WinForm.NET開發中,可以使用一個Panel或UserControl作為一個帶滾動條的容器放置 圖形或其他控件。我們可以設置控件的BackgroundImage屬性來設置控件的背景圖片,但這個 背景圖片是會隨著控件內容的滾動而滾動的,而且還出現背景破碎的不良效果。現筆者在開 發實踐中遇到控件的背景圖片不隨著控件的滾動而滾動。
在B/S開發中,開發者可以 使用“background-attachment:fixed”的CSS樣式來固定HTML文檔的背景圖片, 使之不隨著內容的滾動而滾動。但在WinForm.NET開發中卻沒這個功能。
於是袁某人 又開始路漫漫其修遠兮,到處上下而求索,居然得出了一個解決方法,在此使用發現問題, 分析問題和解決問題的步驟來一一道來,希望能為遇到相同問題的人一點啟發。
發現 問題
首先說說WinForm.NET滾動時背景也隨之滾動的原理。如下圖所示,筆者在一 個窗體上放置一個Panel控件,設置了一個尺寸較大的背景圖片,然後設置控件的AuotScroll 值為true,設置控件的AutoScrollMinSize屬性值為背景圖片的大小,則這個控件就會如下圖 所示的顯示滾動條。
筆者向 下拖拽控件的垂直滾動條,使得控件的內容發生滾動。在默認情況下,Windows操作系統會自 動實現控件客戶區視圖的滾動,例如滾動操作導致了100個像素的滾動距離,Windows會自動 的將控件客戶區顯示的內容向上平移100個像素,於是控件下方新騰出來100個像素高度的客 戶區,這個客戶區就是控件的無效矩形,Windows操作系統會向控件發送WM_PAINT消息,導致 觸發控件的Paint事件,從而調用程序代碼來重新繪制這100個像素高度的區域。
Panel控 件內部處理Paint事件來繪制背景圖片,此時即使控件的內容發生滾動,但繪制圖形使用的XY 坐標系仍然是以控件的左上角為原點的。而且由於無效矩形只有控件客戶區最下面的100個像 素的高度,而無效矩形之上的部分是不會重新繪制的,於是控件重新繪制了一部分的背景圖 片,於是導致如下的用戶界面。這個用戶界面是破碎的,是不能見人的。
筆者讓 其他窗體完全覆蓋掉這個控件後關閉覆蓋窗體,則控件的所有的客戶區都是無效區域, Windows操作系統會向控件發送WM_PAINT消息來觸發控件的Paint事件,而控件內容會自動處 理Paint事件並重新完整的繪制背景,從而形成如下效果。
分析問題
根據上述觀察到的現象可以知道WinForm.NET控件天生具有固定背 景的功能,其背景圖片是不隨控件的滾動而滾動。但Windows的默認滾動圖形的操作卻破壞了 這個功能,從而造成了控件滾動時背景破碎的現象。
根據上述的原理,筆者可以得出 ,只要阻止Windows默認的滾動圖形的操作即可保護WinForm.NET控件的天生的固定背景的功 能,從而實現固定背景的帶滾動的控件。
WinForm.NET並沒有提供任何禁止Windows默 認滾動圖形的功能,於是筆者使用到了一個強大的Win32API函數,那就是LockWindowUpdate 。
這個API函數在C#中的聲明形式如下
[DllImport (“user32.dll”)]
external static bool LockWindowUpdate( IntPtr hWndLock );
這個函數能允許或禁止指定窗體的繪制操作,在任何 時刻,整個操作系統中只能有一個窗體的繪圖操作被禁止掉。
這個函數的參數是窗體 句柄,若參數為0表示用戶界面被鎖定的窗體重新釋放而能繪制用戶界面。
只要在控 件發生滾動時程序調用LockWindowUpdate函數,則控件的內容被鎖定了,不能反映任何圖形 操作,Windows默認的滾動圖形的操作就沒有效果。當控件的滾動操作完成調用 LockWindowUpdate函數來重新釋放窗體並強制重新繪制控件的所有內容,則就能實現固定背 景的效果。
根據上述分析,筆者只要處理控件的滾動事件,當控件內容發生滾動時調 用LockWindowUpdate函數鎖定控件用戶界面,而滾動完畢後又調用LockWindowUpdate函數解 除鎖定並重新繪制控件所有的內容則就可以讓控件發送滾動時不背景圖片不隨之滾動。
在WinForm.NET2.0中,支持滾動的控件都是從 System.Windows.Forms.ScrollableControl派生的,這些控件都提供一個Scroll事件。這個 事件的參數是一個System.Windows.Forms.ScrollEventArgs類型的對象,該參數有一個Type 屬性值,是System.Windows.Forms.ScrollEventType類型,用於表示滾動事件的類型。 WinForm.NET2.0中支持的滾動事件類型有以下幾種。
滾動事件 類型 說明 SmallDecrement 滾動框移動 了較短的距離。用戶單擊了左(水平)或上(垂直)滾動箭頭,或者按了向上鍵。 SmallIncrement 滾動框移動了較短的距離。用戶單擊了右 (水平)或下(垂直)滾動箭頭,或者按了向下鍵。 LargeDecrement 滾動框移動了較長的距離。用戶在滾動條上單擊了滾動框左側 (水平)或上方(垂直),或者按了 Page Up 鍵。 LargeIncrement 滾動框移動了較長的距離。用戶在滾動條上單擊了滾動框右側 (水平)或下方(垂直),或者按了 Page Down 鍵。 ThumbPosition 滾動框被移動。 ThumbTrack 滾動框當前正在移動。 First 滾動框被移動到 System.Windows.Forms.ScrollBar.Minimum 位置。 Last 滾動框被移動到 System.Windows.Forms.ScrollBar.Maximum 位置。 EndScroll 滾動框已停止移動。
一般的用戶在進行滾動操作時會觸發一個或多個不同類型的滾動 事件,而且這些事件的發生過程和Windows操作系統的“拖動時顯示窗口內容”設 置有關。
筆者在控制面板中運行“顯示”項目,顯示出“顯示屬性 ”對話框,切換到“外觀”標簽頁,點擊“效果”按鈕,彈出的 “效果”對話框中就能設置“拖動時顯示窗口內容”的操作系統配置 項了。這個選項對控件的滾動行為影響很大。
在 Windows操作系統中,鼠標右擊控件的滾動條,通常會彈出如下的快捷菜單。點擊這些快捷菜 單就會設置滾動條的位置,而且會觸發不同類型的滾動事件
經過試驗,在WindowsXP SP2的環境下,點擊這些菜單項目而觸發的滾動事件有
滾動至此 當Windows系統設置了“拖動時顯 示窗口內容”時
ThumbTrack
ThumbPosition
ThumbTrack
ThumbPosition
當沒有設置“拖動時顯示窗口內容”時
ThumbTrack
ThumbTrack
ThumbPosition
頂部 First 0 底部 Last 向上翻頁 LargeDecrement 向下翻頁 LargeIncrement 向上滾動 SmallDecrment 向下滾動 SmallIncrement
當用戶用鼠標拖拽操作直接拖動滾動條時,控件 觸發的滾動事件過程如下:
1.當用戶在滾動條上按下鼠標左鍵,開始 拖拽在時,控件觸發ThumbTrack類型的滾動事件。
2.當用戶移動鼠標 時,每一移動都會讓控件觸發ThumbTrack類型的滾動事件。當Windows系統設置了“拖 動時顯示窗口內容”時,還會觸發控件重繪事件,當沒有設置“拖動時顯示窗口 內容”時不會觸發控件重繪事件。
3.當用戶松開鼠標按鍵,結束 拖拽操作時觸發ThumbPosition事件。
此外當程序自己使用代碼設置控件的 AutoScrollPosition屬性來自行滾動時不會觸發任何控件滾動事件,鼠標滾輪操作也不會觸 發滾動事件。
以上是在筆者的WindowsXP SP2的系統中的實驗效果,相信對其他 Windows操作系統也一樣吧。
根據上述實驗結果,筆者重點處理ThumbTrack和 ThumbPosition類型的滾動事件,由於存在“拖動時顯示窗口內容”的設置,筆者 會重載處理控件的Windows消息處理方法,當Windows沒有設置“拖動時顯示窗口內容 ”時,對每一個ThumbTrack事件消息都額外的處理一個ThumbPosition消息,這樣就將 兩種情況統一起來。
解決問題
根據所上述分析,筆者開始創建一種固定背景 的可滾動的控件了,原理上面講的比較清楚,因此編寫代碼時不再多說了。筆者首先使用 VS.NET2005建立一個名為“FixedBackground”的WinForm的C#工程。然後創建一 個名為FixedBackgroundControl的類型,該類型是從System.Windows.Forms.UserControl類 型派生的。
筆者建立一個FixedBackground的屬性用於指定是否啟動固定背景的功能 ,其代碼如下
private bool bolFixedBackground = false;
/// <summary>
/// 固定背景
/// </summary>
[System.ComponentModel.Category("Appearance")]
[System.ComponentModel.DefaultValue(false)]
public bool FixedBackground
{
get
{
return bolFixedBackground;
}
set
{
bolFixedBackground = value;
}
}
然後定義 一個名為LogonImage的屬性用於設置在控件客戶區右下角顯示的圖標,其代碼如下
private System.Drawing.Image myLogonImage = null;
/// <summary>
/// 標志圖片
/// </summary>
[System.ComponentModel.Category("Appearance")]
public System.Drawing.Image LogonImage
{
get
{
return myLogonImage;
}
set
{
myLogonImage = value;
}
}
然後筆者重載控件的 OnPaintBackground方法用於自定義繪制背景,其代碼如下。
/// <summary>
/// 自定義繪制控件背景
/// </summary>
/// <param name="e"></param>
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground (e);
if (myLogonImage != null)
{
// 在控件客戶區的右下角繪制標志圖片
int x = this.ClientSize.Width - myLogonImage.Width;
int y = this.ClientSize.Height - myLogonImage.Height;
if (e.ClipRectangle.IntersectsWith(
new Rectangle(
x,
y,
myLogonImage.Width,
myLogonImage.Height)))
{
e.Graphics.DrawImage(
myLogonImage,
x,
y,
myLogonImage.Width ,
myLogonImage.Height );
}
}
}
接著要處理控件的滾動事件了,首先筆者導入Win32API函數 LockWindowUpdate,其代碼如下
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool LockWindowUpdate(IntPtr hWnd);
筆者重載控件的OnScroll 方法,其代碼如下
/// <summary>
/// 處理滾動條事件
/// </summary>
/// <param name="se">事件參數 </param>
protected override void OnScroll(ScrollEventArgs se)
{
if (bolFixedBackground)
{
// 執行固定背景的操作
if (se.Type == ScrollEventType.ThumbTrack)
{
// 若滾動框正在移動,解除對控件用戶界面的鎖定
LockWindowUpdate(IntPtr.Zero);
// 立即重新繪制控件 所有的用戶界面
this.Refresh();
// 鎖定控件的用戶界面
LockWindowUpdate (this.Handle);
}
else
{
// 解除對控件用戶界面的鎖定
LockWindowUpdate(IntPtr.Zero);
// 聲明 控件的所有的內容無效,但不立即重新繪制
this.Invalidate();
}
}
base.OnScroll(se);
}
在這裡可以看到程序處理滾動事件時調用了 Refresh或Invalidate函數,這將導致控件所有的內容都重新繪制,當控件比較大,內容比較 復雜時會導致繪制控件圖形的任務很重,從而導致明顯的閃爍,這是不可避免的難於優化的 過程,因此筆者使用雙緩沖技術來解決閃爍文件,筆者在控件的構造函數中添加以下代碼即 可啟用控件的雙緩沖設置
// 固定背景圖片會不可避免的導致閃爍,此處啟用 雙緩沖功能。
this.DoubleBuffered = true;
雙緩沖能避免閃爍,但 拖累的軟件的性能,因此本控件是一個為了美觀而降低性能的典型,因此建議本技術不要大 量采用。
接著筆者要重載控件的Windows消息處理方法了,由於要事先知道Windows操作系統的 “拖動時顯示窗口內容”的設置。因此使用了下述代碼
/// <summary>
/// Windows操作系統是否設置為拖動時顯示窗口內容
/// </summary>
private bool bolDragFullWindows = false;
/// <summary>
/// 當創建控件Windows句柄時的處理,調用 SystemParametersInfoGetBool API函數
/// 判斷操作系統是否設置為拖動時顯示窗 口內容。
/// </summary>
/// <param name="e">參數 </param>
protected override void OnHandleCreated(EventArgs e)
{
bolDragFullWindows = false;
if (SystemParametersInfoGetBool(SPI_GETDRAGFULLWINDOWS, 0, ref bolDragFullWindows, 0) == false)
{
bolDragFullWindows = false;
}
base.OnHandleCreated (e);
}
private const int SPI_GETDRAGFULLWINDOWS = 0x0026;
[System.Runtime.InteropServices.DllImport(
"user32.dll",
EntryPoint = "SystemParametersInfo",
SetLastError = true)]
private static extern bool SystemParametersInfoGetBool(
int action,
uint param,
ref bool vparam,
uint init);
在這段代碼中,bolDragFullWindows全局變量用於指明是 否系統是否啟用了“拖動時顯示窗口內容”。筆者重載了OnHandleCreated方法, 在這個方法中調用Win32API函數SystemParametersInfoGetBool來獲得這個系統級設置。
SystemParametersInfo函數是一個很強大的Win32API函數,用於獲得各種操作系統級 設置,能實現的功能點很多,具體可參考MSDN中關於該函數的詳細說明。
經過試驗, 控件在創建後修改了“滾動時顯示窗口內容”後,對控件的滾動行為沒有發生任 何影響。因此實時的檢測“滾動時顯示窗口內容”的設置是不合適的,應當在控 件句柄創建時才檢查該設置,因此筆者重載了控件的OnHandleCreated函數來檢測該系統級設 置並保存在一個全局變量中。
筆者重載了控件的WndProc方法來處理控件接受的 Windows底層消息,其代碼如下
/// <summary>
/// 處理底層 Windows消息處理方法
/// </summary>
/// <param name="m">Windows消息對象</param>
protected override void WndProc(ref Message m)
{
if (bolFixedBackground)
{
if (m.HWnd == this.Handle)
{
// 當前消息是橫向滾動條或縱向滾動條事件
if (m.Msg == 0x0114 // WM_HSCROLL
|| m.Msg == 0x0115)// WM_VSCROLL )
{
int v = m.WParam.ToInt32();
if ((v & 0xf) == 5)
{
// 滾動消息是 THUMBTRACK 類型
base.WndProc(ref m);
if (bolDragFullWindows == false)
{
// Windows操作系統沒有設置為拖動時顯示窗口內容
// 則重復執行 THUMBPOSITION 類型 的滾動消息
v = v - 1;
m.WParam = new IntPtr(v);
base.WndProc(ref m);
}
}
else
{
base.WndProc(ref m);
}
return;
}
}
}
base.WndProc(ref m);
}
在這段代碼中,若消息類型是ThumbTrack類 型的滾動消息,則執行控件的默認處理方法,然後將消息類型修改為ThumbPosition類型的滾 動消息,然後再次執行控件默認的消息處理方法,這樣就使得控件每接受到一個ThumbTrack 類型的滾動消息就處理了ThumbTrack和ThumbPosition兩個消息。這就統一了有和沒有 “拖動時顯示窗口內容”兩種情況。
由於用戶進行鼠標滾輪操作時不會觸 發滾動事件,因此還需要處理控件的鼠標滾輪事件來,其代碼如下
/// <summary>
/// 處理鼠標滾輪事件
/// </summary>
/// <param name="e"></param>
protected override void OnMouseWheel(MouseEventArgs e)
{
if (bolFixedBackground)
{
LockWindowUpdate(this.Handle);
base.OnMouseWheel(e);
LockWindowUpdate(IntPtr.Zero);
this.Invalidate();
}
else
{
base.OnMouseWheel(e);
}
}
筆者還進行了一些其他非關鍵的代碼的編寫,這樣,一個具有固定背景圖片 的可滾動的控件開發完畢。
測試
這個控件開發完畢後,筆者就可以測試了。 筆者在項目中新增一個窗體,打開窗體設計器,可以在工具箱上看到 “FixedBackgroundControl”的項目,筆者點擊該項目即可在窗體上畫出一個固 定背景圖片的控件,則窗體的設計樣式如下圖所示
筆者對 該控件進行以下屬性設置
AutoScroll True AutoScrollMinSize 2000,2000 FixedBackground True LogonImage
這樣筆者就可以運行這個窗體來檢查控件的運行時效果了。
小結
在這篇文章中,筆者使用了WinForm.NET2.0來實現一個具有固定背景圖 片的帶滾動的控件,從而實現了類似“background-attachment:fixed”的CSS樣 式的用戶界面,這個過程是比較復雜的,需要了解WIN32編程的一些知識。相對於常規 ASP.NET開發,C#圖形編程比較復雜的,應用廣泛,對於軟件技術愛好者來說是一片廣闊的天 空。
本文配套源碼