WinForm 控件 (Panel/UserControl/Form) 設定 BackgroundImage 後,預設會**隨內容滾動而滾動**,且滾動過程中因 Windows 預設位移複製機制導致背景破碎。Web 中可用 background-attachment: fixed 固定背景,WinForm 無內建屬性。本文方案:攔截滾動事件,呼叫 Win32 LockWindowUpdate **鎖定視窗更新**,阻止 Windows 預設位移複製,滾動結束後強制全域重繪,實現 **背景固定、前景滾動** 效果。
WM_PAINT 通知控件重繪 **無效區域** (僅底部 100px)。OnPaintBackground 以控件左上角 (0,0) 為原點繪製背景 → **僅重繪底部 100px 背景** → 背景破碎、錯位。核心洞見:控件**天生支援固定背景** (繪圖座標系固定在控件原點),是 **Windows 預設位移複製破壞** 了這特性。只要**阻止位移複製**,背景即固定。
[DllImport("user32.dll")]
private static extern bool LockWindowUpdate(IntPtr hWndLock);
hWndLock = 控件 Handle → **鎖定該視窗所有繪圖操作** (含位移複製)。hWndLock = IntPtr.Zero → **解鎖**,恢復繪圖。
public class FixedBackgroundControl : UserControl
{
// 1. 屬性
private bool _fixedBackground = false;
[Category("Appearance"), DefaultValue(false)]
public bool FixedBackground { get => _fixedBackground; set => _fixedBackground = value; }
private Image _logoImage;
[Category("Appearance")]
public Image LogoImage { get => _logoImage; set => _logoImage = value; }
// 2. 建構函式:啟用雙緩衝對抗閃爍
public FixedBackgroundControl()
{
DoubleBuffered = true; // 必須,否則頻繁 Invalidate/Refresh 會劇烈閃爍
}
// 3. 自訂背景繪製:右下角 Logo
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
if (_logoImage != null)
{
int x = ClientSize.Width - _logoImage.Width;
int y = ClientSize.Height - _logoImage.Height;
var logoRect = new Rectangle(x, y, _logoImage.Width, _logoImage.Height);
if (e.ClipRectangle.IntersectsWith(logoRect))
e.Graphics.DrawImage(_logoImage, x, y);
}
}
// 4. 攔截滾動事件 (ScrollableControl 提供)
protected override void OnScroll(ScrollEventArgs se)
{
if (!_fixedBackground) { base.OnScroll(se); return; }
if (se.Type == ScrollEventType.ThumbTrack) // 正在拖動滑塊
{
LockWindowUpdate(IntPtr.Zero); // 解鎖
Refresh(); // 立即全域重繪 (同步 Paint)
LockWindowUpdate(Handle); // 鎖定
}
else // 箭頭點擊、PageUp/Down、鍵盤、滾動結束
{
LockWindowUpdate(IntPtr.Zero); // 解鎖
Invalidate(); // 標記全域無效,非立即重繪 (效能較好)
}
base.OnScroll(se);
}
// 5. 處理系統「拖動時顯示視窗內容」設定差異
private bool _dragFullWindows = false;
private const int SPI_GETDRAGFULLWINDOWS = 0x0026;
[DllImport("user32.dll", EntryPoint = "SystemParametersInfo", SetLastError = true)]
private static extern bool SystemParametersInfoGetBool(int action, uint param, ref bool vparam, uint init);
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
SystemParametersInfoGetBool(SPI_GETDRAGFULLWINDOWS, 0, ref _dragFullWindows, 0);
}
// 6. 重寫 WndProc 統一 ThumbTrack/ThumbPosition 處理
protected override void WndProc(ref Message m)
{
if (_fixedBackground && m.HWnd == Handle &&
(m.Msg == 0x0114 /*WM_HSCROLL*/ || m.Msg == 0x0115 /*WM_VSCROLL*/))
{
int wparam = m.WParam.ToInt32();
int scrollCode = wparam & 0xFFFF; // 低字: 滾動代碼
if (scrollCode == 5) // SB_THUMBTRACK (5)
{
base.WndProc(ref m);
if (!_dragFullWindows) // 系統未勾選「拖動時顯示內容」
{
// 手動補發一次 ThumbPosition (SB_THUMBPOSITION = 4)
m.WParam = (IntPtr)((wparam & ~0xFFFF) | 4);
base.WndProc(ref m);
}
return;
}
}
base.WndProc(ref m);
}
// 7. 滾輪滾動同樣需鎖定
protected override void OnMouseWheel(MouseEventArgs e)
{
if (_fixedBackground)
{
LockWindowUpdate(Handle);
base.OnMouseWheel(e);
LockWindowUpdate(IntPtr.Zero);
Invalidate();
}
else base.OnMouseWheel(e);
}
}
| 場景 | 滾動事件類型 | 處理策略 | 原因 |
|---|---|---|---|
| 拖動滑塊 | ThumbTrack (連續觸發) | 解鎖 → Refresh() 同步重繪 → 鎖定 | 即時視覺回饋,防止拖動過程背景撕裂 |
| 點擊箭頭/PageUp/鍵盤/滾輪 | SmallIncrement, LargeDecrement, ThumbPosition, EndScroll... | 解鎖 → Invalidate() 非同步標記無效 | 單次跳躍,無需即時重繪,效能更好 |
| 系統設定差異 | 「拖動時顯示內容」啟用/關閉 | WndProc 補發 ThumbPosition | 關閉時系統不發 ThumbPosition,手動補發統一邏輯 |
FixedBackgroundControl。AutoScroll = TrueAutoScrollMinSize = 2000, 2000 (大於客戶區產生滾動條)FixedBackground = TrueBackgroundImage 選大圖LogoImage 選小圖示 (右下角水印驗證)DoubleBuffered=true) **強制啟用**,否則頻繁 Refresh/Invalidate 會劇烈閃爍。ViewportUnits=Absolute)、**自繪控件** (覆寫 OnPaint 手動計算背景偏移)、或 **WebView2** 承載 HTML/CSS 實現。透過 LockWindowUpdate 鎖定視窗更新,阻止 Windows 預設位移複製機制,配合 OnScroll/WndProc/OnMouseWheel 精細控制鎖定/解鎖/重繪時機,並處理系統「拖動時顯示內容」設定差異,成功在 WinForm 實現 **CSS background-attachment: fixed 等效效果**。方案純 managed + 輕量 P/Invoke,無需第三方庫,適合中小型 WinForm 專案快速集成。