首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# WinForm 實現固定背景圖片的可滾動控件:LockWindowUpdate 方案完整解析

     編輯:關於C#

WinForm 控件 (Panel/UserControl/Form) 設定 BackgroundImage 後,預設會**隨內容滾動而滾動**,且滾動過程中因 Windows 預設位移複製機制導致背景破碎。Web 中可用 background-attachment: fixed 固定背景,WinForm 無內建屬性。本文方案:攔截滾動事件,呼叫 Win32 LockWindowUpdate **鎖定視窗更新**,阻止 Windows 預設位移複製,滾動結束後強制全域重繪,實現 **背景固定、前景滾動** 效果。

問題根因:Windows 預設滾動機制

  1. 使用者拖動滾動條 → 內容需向上移動 100px。
  2. Windows **自動執行 BitBlt 位移複製**:將客戶區現有畫面向上平移 100px,底部騰出 100px 無效區域。
  3. 發送 WM_PAINT 通知控件重繪 **無效區域** (僅底部 100px)。
  4. 控件 OnPaintBackground 以控件左上角 (0,0) 為原點繪製背景 → **僅重繪底部 100px 背景** → 背景破碎、錯位。
  5. 若視窗被遮擋再恢復,整個客戶區無效 → 完整重繪 → 背景恢復正確。

核心洞見:控件**天生支援固定背景** (繪圖座標系固定在控件原點),是 **Windows 預設位移複製破壞** 了這特性。只要**阻止位移複製**,背景即固定。

關鍵 API:LockWindowUpdate

[DllImport("user32.dll")]
private static extern bool LockWindowUpdate(IntPtr hWndLock);
  • hWndLock = 控件 Handle → **鎖定該視窗所有繪圖操作** (含位移複製)。
  • hWndLock = IntPtr.Zero → **解鎖**,恢復繪圖。
  • 全系統同一時間**僅允許一個視窗被鎖定**。

解決方案架構:FixedBackgroundControl


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,手動補發統一邏輯

測試與驗證

  1. 新增 WinForm 專案,建置後工具箱出現 FixedBackgroundControl。
  2. 拖放到 Form,設屬性:
    • AutoScroll = True
    • AutoScrollMinSize = 2000, 2000 (大於客戶區產生滾動條)
    • FixedBackground = True
    • BackgroundImage 選大圖
    • LogoImage 選小圖示 (右下角水印驗證)
  3. 執行,拖動滾動條 → **背景圖片完全靜止,前景內容滾動,右下角 Logo 固定**。

效能與取捨

  • 雙緩衝 (DoubleBuffered=true) **強制啟用**,否則頻繁 Refresh/Invalidate 會劇烈閃爍。
  • 代價:每次滾動觸發**全控件重繪**,大控件/複雜內容會明顯降低幀率、增加 CPU 佔用。**屬「以美觀換效能」典型模式,不建議大量/大面積使用**。
  • 替代方案:若需高效能固定背景,考慮 **WPF** (原生支援 ViewportUnits=Absolute)、**自繪控件** (覆寫 OnPaint 手動計算背景偏移)、或 **WebView2** 承載 HTML/CSS 實現。

總結

透過 LockWindowUpdate 鎖定視窗更新,阻止 Windows 預設位移複製機制,配合 OnScroll/WndProc/OnMouseWheel 精細控制鎖定/解鎖/重繪時機,並處理系統「拖動時顯示內容」設定差異,成功在 WinForm 實現 **CSS background-attachment: fixed 等效效果**。方案純 managed + 輕量 P/Invoke,無需第三方庫,適合中小型 WinForm 專案快速集成。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved