簡體   English   中英

如何在調整窗口大小時平滑丑陋的抖動/閃爍/跳躍,尤其是拖動左/上邊框(Win 7-10;bg、bitblt 和 DWM)?

[英]How to smooth ugly jitter/flicker/jumping when resizing windows, especially dragging left/top border (Win 7-10; bg, bitblt and DWM)?

問題:當我抓住我的 Windows 應用程序的調整大小邊框,尤其是頂部或左邊框,並調整窗口大小時,窗口的內容會在我拖動時“實時”調整大小,但它們以一種可怕的方式調整大小,看起來像即使是最新手的用戶也有一個明顯的錯誤:窗口對面邊緣的內容我正在瘋狂地前后拖動抖動/閃爍/跳躍。 根據情況,該現象可能如下所示:

  • 當我們放慢或停止拖動時,內容似乎會離開窗口邊緣並彈回
  • 似乎被拉進窗口的內容,被不同顏色的邊框間歇性地取代,通常是黑色或白色
  • 一個非常丑陋的“雙重圖像”,內容的兩個重疊副本被移動的距離與我們拖動的速度/速度成正比

一旦我停止拖動,丑陋的現象就會停止,但在拖動過程中它使應用程序看起來很業余和不專業。

毫不誇張地說,這個 Windows 問題已經讓數以千計的應用程序開發人員抓狂了

以下是該現象的兩張示例圖片,為Roman Starkov提出的相關問題精心准備

抖動:
示例 1:抖動

邊界:
示例 2:邊框

另一個例子顯示了Kenny Liu的邪惡“雙重圖像”現象(注意快速閃光):

示例 2:雙重圖像

任務管理器現象的另一個示例視頻在這里

問題:任何遇到此問題的開發人員都會很快發現,至少有 30 個 Stack Overflow 問題,有些是最近的,有些是 2008 年的,其中充滿了聽起來很有希望但很少奏效的答案。 現實情況是,這個問題有很多原因,現有的 Stack Overflow 問題/答案從未使更廣泛的上下文變得清晰。 這個問題試圖回答:

  • 這種丑陋的抖動/閃爍/跳躍的最可能原因是什么?
  • 我怎么知道我看到的是哪個原因?
  • 這是特定於特定圖形驅動程序的原因還是 Windows 的一般原因?
  • 我如何解決每個原因? 一個應用程序可以修復它嗎?

(這是一個規范的問答,用於解釋窗口調整大小抖動的所有不同原因,以便用戶可以確定是哪個原因導致了他們的問題並解決了它。正如答案所解釋的,上面的所有排列(本機/托管,窗口/dialog, XP-10) 歸結為只有兩個根本原因,但確定您擁有的是哪個是棘手的部分。)

這個問題的范圍對於這個問題的范圍,這種現象發生在:

  • 本機 Win32 和托管 .NET/WPF/Windows 窗體應用程序
  • 普通的 Win32 窗口和 Win32 對話框窗口
  • Windows 版本,包括 XP、Vista、7、8 和 10(但請參閱下文了解多種原因的黑暗真相)

不在這個問題的范圍內:

  • 如果您的應用程序有一個或多個子窗口(子 HWND),則此問題中的信息對您很有用(因為我們將描述的引起混蛋的BitBlts與父窗口一起應用於您的子窗口),但在窗口調整大小期間您還有一個額外的問題需要處理,這超出了本問題的范圍:您需要讓所有子窗口自動移動並與父窗口同步。 對於此任務,您可能需要BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos ,您可以在此處此處找到有關它們的信息

  • 這個問題假設如果你的應用程序使用 GDI、DirectX 或 OpenGL 繪制到一個窗口,那么你已經在wndproc中實現了一個WM_ERASEBKGND處理程序, wndproc返回WM_ERASEBKGND是 Windows 3.1 的神秘 Windows 殘余,在WM_PAINT之前給出在您繪制窗口之前,您的應用程序有機會“擦除窗口的背景”......嗯。 如果您讓WM_ERASEBKGND消息進入DefWindowProc() ,這將導致您的整個窗口在每次重繪時都被繪制為純色,通常為白色,包括在實時窗口調整大小期間發生的重繪。 結果是丑陋的全窗口閃爍,但不是我們在這個問題中談論的抖動/閃爍/跳躍類型。 攔截WM_ERASEBKGND立即修復了這個問題。

  • 這個問題主要是關於通過用鼠標拖動窗口邊框來實時調整大小。 然而,這里寫的大部分內容也適用於當應用程序使用SetWindowPos()手動調整一次性窗口大小時您可以看到的丑陋工件。 但它們不太明顯,因為它們只在屏幕上輕彈一瞬間,而不是長時間拖動。

  • 這個問題不是關於如何讓你的特定於應用程序的繪圖代碼運行得更快,盡管這樣做在許多情況下可能是解決丑陋的調整大小問題的方法。 如果您的應用程序在實時窗口調整大小期間確實需要大量時間來重新顯示其內容,請考慮優化您的繪圖代碼,或者至少在調整大小期間通過攔截WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE消息來切換到更快、質量較低的繪圖模式以進行檢測調整大小。

  • 如果您的應用程序在調整大小期間根本無法調整大小(例如,在調整大小期間“掛起”,特別是如果它是使用 GLFW 或其他庫的 OpenGL),請參閱這些其他問題,這些問題解釋了在拖動期間 Microsoft 在WM_SYSCOMMAND內的可怕的嵌套/模態事件循環: 這里特別是這個很好的答案這里這里這里這里

第 2 部分:識別和修復 Windows 調整大小問題

注意:您想先閱讀第 1 部分,以使此答案有意義。

這個答案不會解決您所有的調整大小問題。

它組織了來自其他帖子的仍然可用的想法,並添加了一些新穎的想法。

微軟的 MSDN 上根本沒有記錄這種行為,下面是我自己的實驗和查看其他 StackOverflow 帖子的結果。

2a. 來自SetWindowPos() BitBlt和背景填充的調整大小問題

以下問題發生在所有版本的 Windows 上 它們可以追溯到 Windows 平台 (Windows XP) 上實時滾動的最初幾天,並且仍然存在於 Windows 10 上。在較新的 Windows 版本中,其他調整大小問題可能會疊加在此問題之上,如下文所述。

以下是與單擊窗口邊框並拖動該邊框的典型會話相關聯的 Windows 事件。 縮進表示嵌套wndproc (由於已發送(未發布)消息或由於上述問題中“不在此問題范圍內”中提到的可怕的 Windows 模式事件循環而嵌套):

msg=0xa1 (WM_NCLBUTTONDOWN)  [click mouse button on border]
  msg=0x112 (WM_SYSCOMMAND)  [window resize command: modal event loop]
    msg=0x24 (WM_GETMINMAXINFO)
    msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x231 (WM_ENTERSIZEMOVE)      [starting to size/move window]
    msg=0x231 (WM_ENTERSIZEMOVE) done
    msg=0x2a2 (WM_NCMOUSELEAVE)
    msg=0x2a2 (WM_NCMOUSELEAVE) done

  loop:
    msg=0x214 (WM_SIZING)             [mouse dragged]
    msg=0x214 (WM_SIZING) done
    msg=0x46 (WM_WINDOWPOSCHANGING)
      msg=0x24 (WM_GETMINMAXINFO)
      msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x46 (WM_WINDOWPOSCHANGING) done
    msg=0x83 (WM_NCCALCSIZE)
    msg=0x83 (WM_NCCALCSIZE) done
    msg=0x85 (WM_NCPAINT)
    msg=0x85 (WM_NCPAINT) done
    msg=0x14 (WM_ERASEBKGND)
    msg=0x14 (WM_ERASEBKGND) done
    msg=0x47 (WM_WINDOWPOSCHANGED)
      msg=0x3 (WM_MOVE)
      msg=0x3 (WM_MOVE) done
      msg=0x5 (WM_SIZE)
      msg=0x5 (WM_SIZE) done
    msg=0x47 (WM_WINDOWPOSCHANGED) done
    msg=0xf (WM_PAINT)                    [may or may not come: see below]
    msg=0xf (WM_PAINT) done
goto loop;

    msg=0x215 (WM_CAPTURECHANGED)       [mouse released]
    msg=0x215 (WM_CAPTURECHANGED) done
    msg=0x46 (WM_WINDOWPOSCHANGING)
      msg=0x24 (WM_GETMINMAXINFO)
      msg=0x24 (WM_GETMINMAXINFO) done
    msg=0x46 (WM_WINDOWPOSCHANGING) done
    msg=0x232 (WM_EXITSIZEMOVE)
    msg=0x232 (WM_EXITSIZEMOVE) done  [finished size/moving window]
  msg=0x112 (WM_SYSCOMMAND) done
msg=0xa1 (WM_NCLBUTTONDOWN) done

每次拖動鼠標時,Windows 都會為您提供上面循環中顯示的一系列消息。 最有趣的是,你得到WM_SIZING然后WM_NCCALCSIZE然后WM_MOVE/WM_SIZE ,那么你可能(低於更多)收到WM_PAINT

請記住,我們假設您提供了一個返回 1 的WM_ERASEBKGND處理程序(請參閱上面問題中的“不在此問題范圍內”),因此該消息不執行任何操作,我們可以忽略它。

在處理這些消息期間(在WM_WINDOWPOSCHANGING返回后不久),Windows 對SetWindowPos()進行內部調用以實際調整窗口大小。 SetWindowPos()調用首先調整非客戶區(例如標題欄和窗口邊框)的大小,然后將注意力轉向客戶區(您負責的窗口的主要部分)。

在一次拖動的每個消息序列中,Microsoft 都會給您一定的時間來自行更新客戶區。

WM_NCCALCSIZE返回后,這個截止日期的時鍾顯然開始滴答作響。 在 OpenGL 窗口的情況下,當您調用SwapBuffers()以呈現新緩沖區時(而不是在輸入或返回WM_PAINTSwapBuffers() ,截止日期顯然已滿足。 我不使用 GDI 或 DirectX,所以我不知道對SwapBuffers()的等效調用是什么,但您可能會做出很好的猜測,您可以通過在代碼中的各個點插入Sleep(1000)來驗證當以下行為被觸發時。

你有多少時間來滿足你的截止日期? 根據我的實驗,這個數字似乎在 40-60 毫秒左右,但考慮到 Microsoft 經常使用的各種惡作劇,如果這個數字取決於您的硬件配置甚至您的應用程序以前的行為,我不會感到驚訝。

如果您確實在截止日期前更新了您的客戶區,那么 Microsoft 將使您的客戶區保持完好無損。 您的用戶只會看到您繪制的像素,並且您將獲得盡可能平滑的調整大小。

如果您沒有在截止日期前更新您的客戶區,那么 Microsoft 將介入並“幫助”您,首先向您的用戶顯示一些其他像素,基於“填充某些背景顏色”技術的組合(第 1c3 節)第 1 部分)和“切掉一些像素”技術(第 1部分的第 1c4 節)。 Microsoft 向您的用戶顯示的確切像素是復雜的:

  • 如果您的窗口的WNDCLASS.style包含CS_HREDRAW|CS_VREDRAW位(您將WNDCLASS結構傳遞給RegisterClassEx ):

    • 一些令人驚訝的合理的事情發生了。 您將獲得第 1 部分的圖 1c3-1、1c3-2、1c4-1 和 1c4-2 中所示的邏輯行為。 擴大客戶區時,Windows 將在您拖動的窗口的同一側用“背景顏色”(見下文)填充新暴露的像素。 如果需要(左邊框和上邊框),Microsoft 會使用BitBlt來完成此操作。 縮小客戶區時,Microsoft 將在您拖動的窗口的同一側切掉像素。 這意味着您避免了真正令人發指的工件,它使您的客戶區中的對象看起來朝一個方向移動,然后又朝另一個方向移動。

    • 這可能足以為您提供可通過的調整大小行為,除非您真的想推動它並查看是否可以在您有機會繪制之前完全防止 Windows 騷擾您的客戶區(見下文)。

    • 在這種情況下,不要實現您自己的WM_NCCALCSIZE處理程序,以避免下面描述的錯誤的 Windows 行為。

  • 如果您的窗口的WNDCLASS.style不包括CS_HREDRAW|CS_VREDRAW位(包括對話框,Windows 不允許您設置WNDCLASS.style ):

    • Windows 嘗試通過執行BitBlt來“幫助”您,該BitBlt從舊客戶區復制某個像素矩形並將該矩形寫入新客戶區中的某個位置。 這個BitBlt是 1:1(它不會縮放或縮放你的像素)。

    • 然后,Windows 用“背景色”填充新客戶區的其他部分(Windows 在BitBlt操作期間沒有覆蓋的部分)。

    • BitBlt操作通常是調整大小看起來如此糟糕的關鍵原因。 這是因為 Windows 對您的應用程序在調整大小后將如何重繪客戶區做出了錯誤的猜測。 Windows 將您的內容放在錯誤的位置。 最終結果是,當用戶首先看到BitBlt像素,然后看到由您的代碼繪制的真實像素時,您的內容似乎首先向一個方向移動,然后向另一個方向猛拉。 正如我們在第 1 部分中解釋的那樣,這會創建最可怕的調整大小工件類型。

    • 因此,修復調整大小問題的大多數解決方案都涉及禁用BitBlt

    • 如果您實現WM_NCCALCSIZE處理程序並且該處理程序在wParam為 1 時返回WVR_VALIDRECTS ,則您實際上可以控制 Windows 從舊客戶區復制哪些像素 ( BitBlts ) 以及 Windows 在新客戶區中放置這些像素的位置。 WM_NCCALCSIZE只是勉強記載,但看到有關提示WVR_VALIDRECTSNCCALCSIZE_PARAMS.rgrc[1] and [2]在MSDN頁WM_NCCALCSIZENCCALCSIZE_PARAMS 您甚至可以提供NCCALCSIZE_PARAMS.rgrc[1] and [2]返回值,以完全阻止 Windows 將舊客戶區的任何像素BitBlting到新客戶區,或導致 Windows 從和到同一位置BitBlt一個像素,這實際上是同一件事,因為屏幕上的像素不會被修改。 只需將NCCALCSIZE_PARAMS.rgrc[1] and [2]設置為相同的 1 像素矩形。 與消除“背景顏色”(見下文)相結合,這為您提供了一種方法,可以在您有時間繪制窗口像素之前防止 Windows 騷擾您的窗口像素。

    • 如果您實現了WM_NCCALCSIZE處理程序,並且當wParam為 1 時它返回WVR_VALIDRECTS以外的任何內容,那么您會得到一種(至少在 Windows 10 上)與 MSDN 所說的完全不同的行為。 Windows 似乎忽略了您返回的任何左/右/上/下對齊標志。 我建議你不要這樣做。 特別是流行的 StackOverflow 文章如何在用戶調整對話框大小時強制窗口不重繪對話框中的任何內容? 返回WVR_ALIGNLEFT|WVR_ALIGNTOP並且至少在我的 Windows 10 測試系統上現在這似乎完全被破壞了。 如果更改為返回WVR_VALIDRECTS ,那篇文章中的代碼可能會起作用。

    • 如果您沒有自己的自定義WM_NCCALCSIZE處理程序,您會得到一個可能最好避免的非常無用的行為:

      • 如果您縮小客戶區,則什么也不會發生(您的應用根本沒有WM_PAINT )! 如果您使用頂部或左邊框,您的客戶區內容將與客戶區的左上角一起移動。 為了在縮小窗口時實時調整大小,您必須手動從wndproc消息(如WM_SIZE繪制,或調用InvalidateWindow()以觸​​發稍后的WM_PAINT

      • 如果您擴大客戶區

        • 如果拖動底部或右側窗口邊框,Microsoft 會用“背景色”填充新像素(見下文)

        • 如果拖動頂部或左側窗口邊框,Microsoft 會將現有像素復制到展開窗口的左上角,並在新打開的空間中留下舊像素的舊垃圾副本

因此,從這個骯臟的故事中可以看出,似乎有兩種有用的組合:

  • 2a1. 帶有CS_HREDRAW|CS_VREDRAW WNDCLASS.style為您提供第 1 部分的圖 1c3-1、1c3-2、1c4-1 和 1c4-2 中的行為,這並不完美,但至少您的客戶區內容不會向一個方向移動向另一個方向猛拉

  • 2a2. WNDCLASS.style沒有CS_HREDRAW|CS_VREDRAW加上WM_NCCALCSIZE處理程序返回WVR_VALIDRECTS (當wParam為 1 時) BitBlts什么都沒有,加上禁用“背景顏色”(見下文)可能會完全禁用 Windows 對您的客戶區的騷擾。

顯然還有另外一種方式可以達到組合2a2的效果。 您可以攔截WM_WINDOWPOSCHANGING (首先將其傳遞給DefWindowProc )並設置WINDOWPOS.flags |= SWP_NOCOPYBITS ,而不是實現您自己的WM_NCCALCSIZE ,這將禁用 Windows 在窗口調整大小期間對SetWindowPos()的內部調用中的BitBlt 我自己沒有嘗試過這個技巧,但許多 SO 用戶報告說它有效。

在上面的幾個點中,我們提到了“背景顏色”。 此顏色由您傳遞給RegisterClassExWNDCLASS.hbrBackground字段決定。 該字段包含一個HBRUSH對象。 大多數人使用以下樣板代碼設置它:

wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

COLOR_WINDOW+1咒語為您提供白色背景色。 有關 +1 的解釋,請參閱 MSDN dox for WNDCLASS ,並注意 StackOverflow 和 MS 論壇上有很多關於 +1 的錯誤信息。

您可以像這樣選擇自己的顏色:

wndclass.hbrBackground = CreateSolidBrush(RGB(255,200,122));

您還可以使用以下方法禁用背景填充:

wndclass.hbrBackground = NULL;

這是上述組合 2a2 的另一個關鍵成分。 但請注意,新發現的像素將呈現一些本質上隨機的顏色或圖案(無論圖形幀緩沖區中出現什么垃圾),直到您的應用程序趕上並繪制新的客戶區像素,因此實際上使用組合 2a1 和選擇適合您的應用的背景顏色。

2b. 從 DWM 組合填充調整大小問題

在 Aero 開發過程中的某個時刻,微軟在上述全 Windows 版本問題之上添加了另一個實時調整大小抖動問題。

閱讀早期的 StackOverflow 帖子,實際上很難說這個問題是什么時候引入的,但我們可以說:

  • 這個問題肯定會出現在 Windows 10 中
  • 這個問題幾乎肯定會在 Windows 8 中發生
  • 這個問題可能也發生在啟用 Aero 的 Windows Vista 中(許多在 Vista 下有調整大小問題的帖子都沒有說明是否啟用了 Aero)。
  • 即使啟用了 Aero,Windows 7 下也可能不會出現此問題。

問題圍繞着 Microsoft 在 Windows Vista 中引入的稱為DWM 桌面組合的體系結構的重大變化。 應用程序不再直接繪制到圖形幀緩沖區。 相反,所有應用程序實際上都在繪制一個離屏幀緩沖區,然后由 Windows 的新的、邪惡的桌面窗口管理器 (DWM) 進程與其他應用程序的輸出合成。

因此,因為顯示像素涉及另一個過程,所以還有另一個機會弄亂您的像素。

微軟絕不會錯過這樣的機會。

以下是 DWM Compostion 顯然發生的情況:

  • 用戶在窗口邊框上單擊鼠標並開始拖動鼠標

  • 每次用戶拖動鼠標時,都會觸發我們在上面第 2a 節中描述的應用程序中的wndproc事件序列。

  • 但是,與此同時,DWM(記住它是一個與您的應用程序異步運行的單獨進程)啟動自己的截止時間計時器。

  • 與上面的第 2a 節類似,計時器顯然在WM_NCCALCSIZE返回后開始計時,並且在您的應用程序繪制和調用SwapBuffers()

  • 如果更新的截止時間你的客戶區域,然后DWM會離開你的客戶區精美不受干擾。 您的客戶區仍有可能受到第 2a 節中的問題的騷擾,因此請務必閱讀第 2a 節。

  • 如果你沒有在截止日期前更新你的客戶區,那么微軟將做一些真正可怕且令人難以置信的糟糕事情(微軟沒有吸取教訓嗎?):

    • 假設這是調整大小之前的客戶區,其中 A、B、C 和 D 代表客戶區頂部、左側、右側和底部邊緣中間的像素顏色:
    \n   ---------------AAA-----------------\n   |  |\n  公元前\n  公元前\n  公元前\n   |  |\n   --------------DDD-----------------\n  
    • 假設您正在使用鼠標在兩個維度上放大您的客戶區。 Genius Windows DWM(或者可能是 Nvidia:稍后會詳細介紹)將始終將您的客戶區的像素復制到新客戶區的左上角(無論您拖動哪個窗口邊框),然后做最荒謬的事情可以想象到客戶區的其余部分。 Windows 將沿客戶區底部邊緣使用任何像素值,將它們拉伸到新的客戶區寬度(我們在第 1部分第 1c2 節中探討的一個糟糕的想法,並復制這些像素以填充所有新的客戶區寬度)在底部打開空間(看看 D 會發生什么)。然后 Windows 將采用沿客戶區右邊緣的任何像素值,將它們拉伸到新的客戶區高度,並復制它們以填充新的右上角的開放空間:
    \n   --------------AAA----------------------------------- ------------\n   |  |  |\n  卑詩省 |\n  卑詩省 |\n   B CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n   |  |CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n   --------------DDD-----------------CCCCCCCCCCCCCCCCCCCCCCCCCCCC\n   | 滴滴滴滴滴|\n   | 滴滴滴滴滴|\n   | 滴滴滴滴滴|\n   | 滴滴滴滴滴|\n   | 滴滴滴滴滴|\n   ------------------------------DDDDDDDDD------------------- ------\n  
    • 我什至無法想象他們在抽什么煙。 在許多情況下,這種行為會產生最壞的結果。 首先,當拖動左側和頂部窗口邊框時,幾乎可以保證產生我們在第 1 部分的圖 1c3-3 和圖 1c4-3 中展示的可怕的來回運動,因為復制的矩形總是在左上角無論您拖動的是哪個窗口邊框。 其次,如果您碰巧在那里設置了除背景顏色以外的任何像素,那么復制邊緣像素時發生的更荒謬的事情將產生丑陋的條。 請注意創建的 C 和 D 條甚至與復制的舊像素中的原始 C 和 D 不對齊。 我可以理解他們為什么要復制邊緣,希望在那里找到背景像素以“自動化”背景顏色檢測過程,但似乎這種實際工作的可能性被黑客因素和失敗的可能性大大超過了。 如果 DWM 使用應用程序選擇的“背景顏色”(在WNDCLASS.hbrBackground )會更好,但我懷疑 DWM 可能無法訪問該信息,因為 DWM 處於不同的進程中,因此是 hack。 嘆。

但我們甚至還沒有到最糟糕的部分:

  • 在 DWM 通過這種笨拙的猜測破壞它之前,DWM 給您繪制自己的客戶區的最后期限實際上是多少? 顯然(根據我的實驗)截止日期約為 10-15 毫秒 鑒於 15 毫秒接近 1/60,我猜測截止日期實際上是當前幀的結束。 絕大多數應用程序在大多數情況下都無法滿足這個期限。

這就是為什么,如果您在 Windows 10 上啟動 Windows 資源管理器並拖動左邊框,您很可能會看到右側的滾動條不規則地抖動/閃爍/跳躍,就好像 Windows 是由四年級學生編寫的一樣。

我不敢相信微軟已經發布了這樣的代碼並認為它​​“完成了”。 也有可能相關代碼在圖形驅動程序中(例如 Nvidia、Intel 等),但一些 StackOverflow 帖子讓我相信這種行為是跨設備的。

在使用左側或頂部窗口邊框調整大小時,您幾乎無法防止這一層無能產生可怕的抖動/閃爍/跳躍。 那是因為對您的客戶區的粗魯、未經同意的修改發生在另一個進程中。

我真的希望某些 StackOverflow 用戶能夠在 Windows 10 中提出一些神奇的 DWM 設置或標志,我們可以使用它們來延長截止日期或完全禁用可怕的行為。

但與此同時,我確實想出了一個技巧,可以在一定程度上降低窗口調整大小期間可怕的來回工件的頻率。

受到https://stackoverflow.com/a/25364123/1046167 中的評論啟發的 hack 是盡最大努力將應用程序進程與驅動 DWM 活動的垂直回溯同步。 實際上,在 Windows 中進行這項工作並非易事。 這個 hack 的代碼應該是WM_NCCALCSIZE處理程序中的最后一件事:

LARGE_INTEGER freq, now0, now1, now2;
QueryPerformanceFrequency(&freq); // hz

// this absurd code makes Sleep() more accurate
// - without it, Sleep() is not even +-10ms accurate
// - with it, Sleep is around +-1.5 ms accurate
TIMECAPS tc;
MMRESULT mmerr;
MMC(timeGetDevCaps(&tc, sizeof(tc)), {});
int ms_granularity = tc.wPeriodMin;
timeBeginPeriod(ms_granularity); // begin accurate Sleep() !

QueryPerformanceCounter(&now0);

// ask DWM where the vertical blank falls
DWM_TIMING_INFO dti;
memset(&dti, 0, sizeof(dti));
dti.cbSize = sizeof(dti);
HRESULT hrerr;
HRC(DwmGetCompositionTimingInfo(NULL, &dti), {});

QueryPerformanceCounter(&now1);

// - DWM told us about SOME vertical blank
//   - past or future, possibly many frames away
// - convert that into the NEXT vertical blank

__int64 period = (__int64)dti.qpcRefreshPeriod;

__int64 dt = (__int64)dti.qpcVBlank - (__int64)now1.QuadPart;

__int64 w, m;

if (dt >= 0)
{
    w = dt / period;
}
else // dt < 0
{
    // reach back to previous period
    // - so m represents consistent position within phase
    w = -1 + dt / period;
}

// uncomment this to see worst-case behavior
// dt += (sint_64_t)(0.5 * period);

m = dt - (period * w);

assert(m >= 0);
assert(m < period);

double m_ms = 1000.0 * m / (double)freq.QuadPart;

Sleep((int)round(m_ms));

timeEndPeriod(ms_granularity);

您可以通過取消注釋顯示“最壞情況”行為的行來說服自己這個 hack 正在起作用,方法是嘗試在幀的中間而不是垂直同步安排繪圖,並注意您還有多少工件。 您還可以嘗試緩慢改變該行中的偏移量,您會看到偽像在大約 90% 的時間段突然消失(但不是完全),並在大約 5-10% 的時間段再次出現。

由於Windows不是一個實時操作系統,可以為您的應用程序將在此代碼的任何地方搶占,導致不准確的配對now1dti.qpcVBlank 這個小代碼段中的搶占很少見,但有可能。 如果你願意,你可以比較now0now1如果結合不夠緊密再次運轉並循環。 另外,也可以用於搶占破壞的定時Sleep()之前或之后或代碼Sleep() 對此您無能為力,但事實證明這部分代碼中的計時錯誤被 DWM 的不確定行為所淹沒; 即使您的時機是完美的,您仍然會得到一些調整窗口大小的偽像。 這只是一個啟發式。

還有第二個技巧,這是一個非常有創意的技巧:如 StackOverflow 帖子中所述,拖動窗口的左邊框時無法消除抖動,您實際上可以在您的應用程序中創建兩個主窗口,並且每次Windows 會執行SetWindowPos ,您接受這一點,而是隱藏一個窗口並顯示另一個! 我還沒有嘗試過這個,但 OP 報告說它繞過了上面描述的瘋狂像素 DWM 像素副本。

還有第三個技巧,它可能會根據您的應用程序起作用(尤其是與上面的計時技巧結合使用)。 在實時調整大小期間(您可以通過攔截WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE來檢測),您可以修改您的繪圖代碼以最初繪制更簡單的東西,這些東西更有可能在問題 2a 和 2b 規定的期限內完成,然后調用SwapBuffers()領取您的獎品:這足以防止 Windows 執行第 2a 和 2b 節中描述的錯誤 blit/fill。 然后,在部分繪制之后,立即進行另一次繪制以完全更新窗口內容並再次調用SwapBuffers() 這可能看起來仍然有些奇怪,因為用戶將看到您的窗口更新分為兩部分,但它看起來可能比 Windows 可怕的來回運動偽影要好得多。

還有一點很吸引人:Windows 10 中的一些應用程序,包括控制台(啟動cmd.exe ),即使在拖動左邊框時也沒有 DWM 合成偽影。 所以有一些方法可以繞過這個問題。 讓我們找到它!

2c。 如何診斷您的問題

當您嘗試解決特定的調整大小問題時,您可能想知道您看到的是第 2a 節和第 2b 節中的哪些重疊效果。

將它們分開的一種方法是在 Windows 7 上調試一下(為了安全起見,禁用 Aero 的情況下)。

快速確定您是否在第 2b 節中看到問題的另一種方法是修改您的應用程序以顯示第 2b 節中描述的測試模式,如下例所示(注意四個邊緣中每條邊上的 1 像素細彩色線):

測試模式

然后抓住任何窗口邊框並開始快速調整該邊框的大小。 如果您看到間歇性的巨大彩色條(在此測試模式中為藍色或綠色條,因為底部邊緣為藍色,右側邊緣為綠色),那么您就知道您在第 2b 節中看到了問題。

您可以通過將WNDCLASS.hbrBackground設置為不同的背景顏色(如紅色)來測試您是否在第 2a 節中看到了問題。 當您調整窗口大小時,新暴露的部分將顯示為該顏色。 但是通讀第 2a 節以確保您的消息處理程序不會導致 Windows 對整個客戶區進行BitBlt ,這會導致 Windows 不繪制任何背景顏色。

請記住,第 2a 節和第 2b 節中的問題僅在您的應用程序未能在特定截止日期前繪制時出現,並且每個問題都有不同的截止日期。

因此,如果不進行修改,您的應用程序可能只會顯示第 2b 節的問題,但是如果您修改應用程序以使其繪制更慢(例如,在SwapBuffers()之前的WM_PAINT插入Sleep() ),您可能會錯過第 2a 節和第 2b 節的最后期限第 2b 節並開始同時看到這兩個問題。

當您在較慢的DEBUG構建和RELEASE構建之間更改應用程序時,也可能會發生這種情況,這會使追逐這些調整大小問題變得非常令人沮喪。 了解幕后發生的事情可以幫助您處理令人困惑的結果。

第 1 部分:是什么讓調整大小看起來好或壞?

StackOverflow 上關於平滑調整大小的問題有太多的歧義和不明確,我們需要建立一個通用詞匯來幫助人們更清晰地回答。

這就是我們在本節中要做的。

為簡單起見,我們將僅在水平維度上解釋平滑調整大小的問題,但這里的所有內容都同樣適用於垂直調整大小。

下面我們將參考一個窗口的

  • “非客戶區”:Windows 管理的窗口部分,包括頂部的標題欄和所有邊緣的窗口邊框,以及

  • “客戶區:”您負責的窗口的主要部分

假設您有一個應用程序:

  • 應該保持左齊平的按鈕或標簽 L
  • 應該保持齊平的按鈕或標簽 R

無論窗口如何調整大小。

您的應用程序可能會自己繪制 L/R(例如在一個窗口內使用 GDI/OpenGL/DirectX),或者 L/R 可能是一些 Microsoft 控件(它有自己的 HWND 與您的主窗口 HWND 分開); 沒關系。

這是應用程序窗口客戶區的簡化表示。 如您所見,我們在客戶區最左側有三列寬的 LLL,在客戶區最右側有三列寬的 RRR,其他各種客戶區內容以“-”表示之間(請忽略 StackOverflow 堅持添加的灰色背景;L 和 R 位於客戶區的最左側和最右側):

LLL-----------RRR

現在想象你抓住這個窗口的左邊界或右邊界並拖動它來使窗口變大或變小。

1a. 簡單案例:按時繪圖

想象一下,您的應用程序繪制速度非常快,因此它始終可以在 1 毫秒內響應用戶的拖動操作,而操作系統讓您的應用程序可以快速繪制,而無需嘗試在屏幕上繪制任何其他內容來“幫助”您。

當您拖動應用程序邊框時,用戶會在屏幕上看到以下內容(這些數字的每一行代表一個時刻):

向右拖動右邊框(擴大寬度):

(Figure 1a-1)
LLL-----------RRR     (initially, when you click the mouse)
LLL------------RRR    (as you drag the mouse)
LLL-------------RRR   (as you drag the mouse)
LLL--------------RRR  (when you release the mouse)

向左拖動右邊框(縮小寬度):

(Figure 1a-2)
LLL-----------RRR
LLL----------RRR
LLL---------RRR
LLL--------RRR

向左拖動左邊框(擴大寬度):

(Figure 1a-3)
   LLL-----------RRR
  LLL------------RRR
 LLL-------------RRR
LLL--------------RRR

將左邊框向右拖動(縮小寬度):

(Figure 1a-4)
LLL-----------RRR
 LLL----------RRR
  LLL---------RRR
   LLL--------RRR

這些看起來都很好而且很流暢:

  • 調整右邊框時,R 似乎在一個方向上以恆定速度移動,而 L 保持原樣不動。
  • 調整左邊框時,L 似乎在一個方向上以恆定速度移動,而 R 保持原樣不動。

到現在為止還挺好。

1b. 硬案例:繪圖落后

現在,想象一下您的應用程序繪制速度如此之慢,以至於當您用鼠標拖動時,該應用程序無法跟上您的步伐。 是的,最終,您的繪圖會趕上來,但我們正在討論在您用手拖動鼠標期間會發生什么。 顯然計算機無法伸手抓住你的手來減慢你的鼠標移動速度,所以關鍵問題是:

  • 在此期間屏幕上應該顯示什么,以及
  • 誰來決定應該展示什么?

例如,向右拖動右邊框時(擴大寬度):

(Figure 1b-1)
LLL-----------RRR
??????????????????    (what should show here?)
???????????????????   (what should show here?)
LLL--------------RRR  (app catches up)

再舉一個例子,當向左拖動左邊框時(縮小寬度):

(Figure 1b-2)
LLL-----------RRR
 ????????????????  (what should show here?)
  ???????????????  (what should show here?)
   LLL--------RRR  (app catches up)

這些是決定運動是否流暢的關鍵問題,也是整個 StackOverflow 問題所圍繞的關鍵問題。

不同版本的 Windows 在不同的上下文中對這些問題提供了不同的答案,這意味着更平滑地調整大小的解決方案取決於您所處的情況。

1c。 等待應用程序繪制時的臨時解決方案

在用戶開始拖動鼠標以調整窗口大小之后,但在您的應用程序通過以新大小繪制窗口來趕上之前的時間段內,有多種選擇。

1c1。 沒做什么

屏幕可以保持原樣,直到應用程序趕上(您的客戶端像素甚至非客戶端區域中的窗口邊框都不會發生變化):

向右拖動右邊框(擴大寬度)時的示例:

(Figure 1c1-1)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
LLL--------------RRR  (app catches up)

向左拖動左邊框時的示例(縮小寬度):

(Figure 1c1-2)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
   LLL--------RRR  (app catches up)

這種方法的明顯缺點是,在相關期間,應用程序似乎“掛起”並且似乎對您的鼠標移動沒有響應,因為 R 和“-”、L 和窗口邊框都沒有移動。

微軟經常被指責為 Windows 是一個沒有響應的操作系統(有時是他們的錯,有時是應用程序開發人員的錯),所以自從微軟推出實時調整大小(Windows XP?)以來,微軟從未使用過“什么都不做”的方法通過它自己。

“什么都不做”的方法對用戶來說很煩人,而且看起來不專業,但事實證明(非常不明顯)它並不總是最糟糕的選擇。 繼續閱讀...

1c2。 縮放內容

另一種可能性是,Windows 總是可以讓窗口邊框立即跟隨您的鼠標移動(因為 Windows 本身有足夠的處理能力,至少可以及時繪制非客戶區),並且在等待您的應用程序時,Windows 可以獲取客戶區域的舊像素並放大或縮小這些像素,就像放大/放大圖像一樣,以便它們“適合”更小或更大的空間。

這種技術通常比任何其他技術都差,因為它會導致原始內容的模糊圖像很可能不成比例。 所以在任何情況下都沒有人應該這樣做。 除了,正如我們將在第 2 部分中看到的,有時微軟會這樣做。

1c3。 放大時填一些背景色

放大窗口時可以使用的另一種技術如下:Windows 始終可以使窗口邊框立即跟隨您的鼠標移動,並且 Windows 可以用一些臨時背景色 B 填充現在更大的客戶區的新像素:

例如,向右拖動右邊框時(擴大寬度):

(Figure 1c3-1)
LLL-----------RRR
LLL-----------RRRB
LLL-----------RRRBB
LLL--------------RRR  (app catches up)

這種方法的優點是在所討論的時間段內,至少您的窗口邊框移動的,因此應用程序感覺響應。

另一個不錯的功能是在拖動過程中,L 保持靜止,就像它應該的那樣。

有點奇怪的是,你在拖動時創建的新空間被一些隨機顏色填充,更奇怪的是 R 直到稍后才真正移動(注意 R 在最后一刻向右猛拉 3 列),但至少 R 只在正確的方向上移動。 這是部分改進。

一個巨大而重要的問題是:新填充的背景色 B 應該是什么顏色? 如果 B 恰好是黑色,而您的應用程序恰好具有大部分白色背景,反之亦然,則比 B 與您現有內容的背景顏色匹配時要丑得多。 正如我們將在第 2 部分中看到的,Windows 已經部署了幾種不同的策略來改進 B 的選擇。

現在考慮相同的想法,但將其應用於我們將左邊框向左拖動(擴大寬度)的情況。

合乎邏輯的事情是在窗口左側填充新的背景顏色:

(Figure 1c3-2)
   LLL-----------RRR
  BLLL-----------RRR
 BBLLL-----------RRR
LLL--------------RRR  (app catches up)

這是合乎邏輯的,因為 R 會保持原狀,就像它應該的那樣。 L 將具有與我們在上面的圖 1c3-1 中描述的相同的怪異(L 會保持靜止,然后在最后一刻突然向左猛拉 3 列),但至少 L 只會向正確的方向移動。

然而——這真的會讓人感到震驚——在你必須處理的幾個重要情況下,Windows 並沒有做合乎邏輯的事情。

相反,即使您拖動左側窗口邊框,Windows 有時也會填充右側的背景像素 B:

(Figure 1c3-3)
   LLL-----------RRR
  LLL-----------RRRB
 LLL-----------RRRBB
LLL--------------RRR  (app catches up)

是的,這太瘋狂了。

考慮一下這對用戶的看法:

  • L 看起來在一個方向上以恆定速度非常平穩地移動,所以這實際上很好,但是

  • 看看R在做什么:

    \n  存款准備金率\n 存款准備金率\n存款准備金率  \n   RRR(應用趕上)\n
    • R 首先向左移動兩列,它不應該這樣做:R 應該始終保持齊平
    • R 然后再次向右 碉堡了!

這看起來太可怕了,可怕的,糟糕的,惡心的,......甚至沒有詞來形容這看起來有多糟糕。

人眼對運動極其敏感,即使是發生在幾幀時間內的運動。 我們的眼睛立即注意到 R 的這種奇怪的來回運動,我們立即知道有些事情嚴重錯誤。

因此,在這里您可以開始了解為什么某些丑陋的調整大小問題僅在您拖動左側(或頂部)邊框而不是右側(或底部)邊框時發生。

實際上,這兩種情況(圖 1c3-2 與圖 1c3-3)都做了一些奇怪的事情。 在圖 1c3-2 中,我們臨時添加了一些不屬於那里的背景像素 B。 但是這種奇怪的行為遠不如圖 1c3-3 的來回運動那么明顯。

這種來回運動許多 StackOverflow 問題所涉及的抖動/閃爍/跳躍。

因此,平滑調整大小問題的任何解決方案都必須:

  • 至少防止您客戶區中的項目出現向一個方向跳躍然后返回另一個方向的情況。

  • 如果可能的話,最好也避免添加背景像素 B

1c4。 收縮時,切掉一些像素

第 1c3 節涉及擴展窗口。 如果我們查看縮小窗口,我們將看到一組完全類似的情況。

縮小窗口時可以使用的技術如下:Windows 始終可以使窗口邊框立即跟隨您的鼠標移動,並且 Windows 可以簡單地切掉(裁剪)您現在更小的客戶區的一些像素。

例如,當向左拖動右邊框時(縮小寬度):

(Figure 1c4-1)
LLL-----------RRR
LLL-----------RR
LLL-----------R
LLL--------RRR     (app catches up)

使用這種技術,L 保持原樣,但右側發生了奇怪的事情:R,無論窗口大小如何,都應該保持齊平,但它的右邊緣似乎被右邊緣逐漸切掉客戶區,直到 R 消失,然后當應用程序趕上時,突然 R 重新出現在正確的位置。 這很奇怪,但請記住,R 似乎在任何時候都不會向右移動。 R 的左邊緣似乎保持靜止,直到所有 R 向左跳回 3 列的最后時刻。 所以,就像我們在圖 1c3-1 中看到的那樣,R 只會在正確的方向上移動。

現在考慮當我們向右拖動左邊框(縮小寬度)時會發生什么。

合乎邏輯的做法是將客戶區左側的像素刮掉:

(Figure 1c4-2)
LLL-----------RRR
 LL-----------RRR
  L-----------RRR
   LLL--------RRR  (app catches up)

這將具有與圖 1c4-1 相同的奇怪屬性,只是左右角色顛倒了。 L 似乎從 L 的左邊緣逐漸被剃光,但 L 的右邊緣將保持靜止,直到最后一刻 L 似乎向右跳。 所以 L 只會朝着正確的方向移動,盡管是突然的。

但是——是的,再次為完全震驚做好准備——在你必須處理的幾個重要情況下,Windows 並沒有做合乎邏輯的事情。

相反,即使您拖動左側窗口邊框,Windows 有時也會從右側切掉像素:

(Figure 1c4-3)
LLL-----------RRR
 LLL-----------RR
  LLL-----------R
   LLL--------RRR  (app catches up)

考慮一下這對用戶的看法:

  • L 看起來在一個方向上以恆定速度非常平穩地移動,所以這實際上很好,但是

  • 看看R在做什么:

    \n存款准備金率\n  RR\n  電阻\n RRR(應用趕上)\n
    • R 首先向右滑動兩列。 R 的左邊緣似乎與 R 的其余部分一起向右移動。
    • R 然后再次向左

正如您在閱讀 1c3 部分后現在應該意識到的那樣,這種來回運動看起來絕對可怕,並且比圖 1c4-1 和圖 1c4-2 公認的怪異行為要糟糕得多。

1c5。 稍等,然后嘗試以上之一

到目前為止,當用戶開始拖動窗口邊框但應用程序尚未重繪時,我們已經提出了不同的想法。

這些方法實際上可以組合使用。

暫時,試着從微軟的角度思考這個問題。 在用戶開始拖動鼠標來調整窗口大小的那一刻,微軟無法提前知道你的應用繪制需要多長時間。 所以微軟必須取得一個平衡:

  • 如果您的應用程序要快速響應,那么微軟對屏幕所做的任何更改都會使您的應用程序看起來比微軟只是讓您繪制真實內容更糟(請記住,上述所有技巧在不同程度上都很奇怪,並且會您的內容看起來很奇怪,所以不使用任何這些技巧肯定會更好)。

  • 但是,如果 Microsoft 等待您繪制的時間過長,您的應用程序(以及擴展的 Windows)將看起來像我們在第 1c1 節中所解釋的那樣卡頓和無響應。 這讓微軟失去面子,即使是你的錯。

所以,另一種選擇是先推遲任何屏幕變化,給應用程序一定的時間來繪制,如果應用程序未能按時完成,則使用上述方法之一暫時“填補空白”。 ”

這對你來說聽起來很可怕嗎? 你猜怎么着? 這就是 Windows 所做的,至少以 2 種不同的方式同時具有 2 個不同的截止時間。 第 2 部分將深入探討這些案例......

第 3 部分:悲傷畫廊:相關鏈接的注釋列表

您可能可以通過查看源材料來收集我遺漏的想法:

2014 與 2017 更新: 拖動窗口左邊框時無法消除抖動:可能是最新的問題,但仍然缺乏上下文; 建議有兩個窗口並在實時調整大小期間交替取消隱藏它們的創造性但相當瘋狂的技巧! 也是我在答案中發現的唯一問題,其中提到了 DWM 中的競爭條件和DwmGetCompositionTimingInfo()的部分時間修復。

2014 為什么每次調整 WPF 窗口大小時都會出現黑色延遲? : 是的 WPF 也這樣做。 沒有有用的答案

2009 如何修復 WPF 表單調整大小 - 控件滯后和黑色背景? : 控制滯后和黑色背景?” 多 HWND 示例。提到WM_ERASEBKGND和背景刷技巧,但沒有現代答案。

2018 使用WPF時有沒有辦法減少或防止表單閃爍? : 是的,截至 2018 年仍未修復。

2018 使用 SetWindowPos 更改窗口左邊緣時減少閃爍:未回答的問題有許多過時的建議,如WM_NCCALCSIZE

2012 OpenGL 閃爍/損壞,窗口調整大小和 DWM 處於活動狀態:很好的問題陳述,回答者完全誤解了上下文並提供了不適用的答案。

2012 如何避免 GUI 調整大小中的瞬時更新? : 提到了攔截WM_WINDOWPOSCHANGING並設置WINDOWPOS.flags |= SWP_NOCOPYBITS

2016 Unity 錯誤報告:“窗口大小調整非常斷斷續續(邊框不能順滑地跟隨鼠標)”在數百個應用程序中發現的典型錯誤報告部分是由於此錯誤報告中的問題,部分是由於某些應用程序具有畫得慢。 我發現的唯一一個文檔實際上說 Windows 10 DWM 會限制並擴展舊窗口的外部像素,我可以確認這一點。

2014 使用 Windows-8 之前的答案(包括CS_HREDRAW/CS_VREDRAWWM_NCCALCSIZE 從左側調整大小時在窗口上閃爍

2013 Resizing Window 導致右邊框附近出現拖尾現象,老式的 Win-7-only 解決方案禁用 Aero。

2018 Flicker-free expand (resize) of a window to left一個多窗口(multi-HWND)案例的例子,沒有真正的答案。

2013 WinAPI C++: Reprogramming Window Resize : 太含糊不清地詢問是關於客戶區閃爍(如這個問題)還是非客戶區閃爍。

2018 GLFW 錯誤“在 Windows 10 上調整窗口大小會顯示跳躍行為”是許多此類錯誤之一,它們從未解釋上下文,就像許多 StackOverflow 帖子一樣

2008 年“Flicker Free Main Frame Resizing” CodeProject實際上執行 StretchBlt 但在 Windows 8+ 世界中不起作用,當屏幕上顯示不正確的像素時,應用程序無法控制。

2014 在 Windows 中平滑調整窗口大小(使用 Direct2D 1.1)? :Windows 8+ DWM 副本的明確但未解決的問題

2010 當用戶調整我的對話框大小時,我如何強制窗口不在我的對話框中重繪任何內容? :WM_NCCALCSIZE 修復了禁用在 Windows 8+ 中不再有效的 bitblt,因為 DWM 在應用程序有機會顯示之前破壞了屏幕。

2014 移動/調整窗口大小時閃爍:以前在 Windows 8+ 中不起作用的修復的綜述。

2007 WinXP時代“減少閃爍”CodeProject推薦WM_ERASEBKGND+SWP_NOCOPYBITS

2008 年早期的Google Bug報告新的 Vista DWM 問題

目錄

因為這是一個復雜的、多方面的問題,我建議按以下順序閱讀答案:

以及可以幫助其他人收集見解的源材料列表:

請隨時以創造性的方式提供更多答案,以避免 2a 中描述的問題,尤其是 2b!

請參閱博客文章平滑調整大小測試,其中有一些分析和解決方案的指針。 基本上有一個獲勝策略,即在實時調整大小時渲染到重定向表面,並在其他時間使用交換鏈。 我不確定這是否能解決您的具體問題,因為您需要對演示文稿的工作方式進行足夠的低級控制才能實現它。 這種方法還假設您正在使用 Direct2D(正如我目前正在做的那樣)或 DirectX 進行繪圖。

如果您使用的是 DXGI,您可以使用 DirectComposition + WS_EX_NOREDIRECTIONBITMAP 完全繞過重定向表面並在從 WM_NCCALCSIZE 返回之前(即在任何截止時間計時器開始之前)渲染/呈現具有新大小的客戶區。 這是使用 D3D11 的最小示例:

#include <Windows.h>

#include <d3d11.h>
#include <dcomp.h>
#include <dxgi1_2.h>

ID3D11Device* d3d;
ID3D11DeviceContext* ctx;
IDXGISwapChain1* sc;

/// <summary>
/// Crash if hr != S_OK.
/// </summary>
void hr_check(HRESULT hr)
{
    if (hr == S_OK) return;
    while (true) __debugbreak();
}

/// <summary>
/// Passthrough (t) if truthy. Crash otherwise.
/// </summary>
template<class T> T win32_check(T t)
{
    if (t) return t;

    // Debuggers are better at displaying HRESULTs than the raw DWORD returned by GetLastError().
    HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
    while (true) __debugbreak();
}

/// <summary>
/// Win32 message handler.
/// </summary>
LRESULT window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
    switch (message)
    {
    case WM_CLOSE:
        ExitProcess(0);
        return 0;

    case WM_NCCALCSIZE:
        // Use the result of DefWindowProc's WM_NCCALCSIZE handler to get the upcoming client rect.
        // Technically, when wparam is TRUE, lparam points to NCCALCSIZE_PARAMS, but its first
        // member is a RECT with the same meaning as the one lparam points to when wparam is FALSE.
        DefWindowProc(hwnd, message, wparam, lparam);
        if (RECT* rect = (RECT*)lparam; rect->right > rect->left && rect->bottom > rect->top)
        {
            // A real app might want to compare these dimensions with the current swap chain
            // dimensions and skip all this if they're unchanged.
            UINT width = rect->right - rect->left;
            UINT height = rect->bottom - rect->top;
            hr_check(sc->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0));

            // Do some minimal rendering to prove this works.
            ID3D11Resource* buffer;
            ID3D11RenderTargetView* rtv;
            FLOAT color[] = { 0.0f, 0.2f, 0.4f, 1.0f };
            hr_check(sc->GetBuffer(0, IID_PPV_ARGS(&buffer)));
            hr_check(d3d->CreateRenderTargetView(buffer, NULL, &rtv));
            ctx->ClearRenderTargetView(rtv, color);
            buffer->Release();
            rtv->Release();

            // Discard outstanding queued presents and queue a frame with the new size ASAP.
            hr_check(sc->Present(0, DXGI_PRESENT_RESTART));

            // Wait for a vblank to really make sure our frame with the new size is ready before
            // the window finishes resizing.
            // TODO: Determine why this is necessary at all. Why isn't one Present() enough?
            // TODO: Determine if there's a way to wait for vblank without calling Present().
            // TODO: Determine if DO_NOT_SEQUENCE is safe to use with SWAP_EFFECT_FLIP_DISCARD.
            hr_check(sc->Present(1, DXGI_PRESENT_DO_NOT_SEQUENCE));
        }
        // We're never preserving the client area so we always return 0.
        return 0;

    default:
        return DefWindowProc(hwnd, message, wparam, lparam);
    }
}

/// <summary>
/// The app entry point.
/// </summary>
int WinMain(HINSTANCE hinstance, HINSTANCE, LPSTR, int)
{
    // Create the DXGI factory.
    IDXGIFactory2* dxgi;
    hr_check(CreateDXGIFactory1(IID_PPV_ARGS(&dxgi)));

    // Create the D3D device.
    hr_check(D3D11CreateDevice(
        NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
        NULL, 0, D3D11_SDK_VERSION, &d3d, NULL, &ctx));

    // Create the swap chain.
    DXGI_SWAP_CHAIN_DESC1 scd = {};
    // Just use a minimal size for now. WM_NCCALCSIZE will resize when necessary.
    scd.Width = 1;
    scd.Height = 1;
    scd.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    scd.SampleDesc.Count = 1;
    scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    scd.BufferCount = 2;
    // TODO: Determine if PRESENT_DO_NOT_SEQUENCE is safe to use with SWAP_EFFECT_FLIP_DISCARD.
    scd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
    scd.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
    hr_check(dxgi->CreateSwapChainForComposition(d3d, &scd, NULL, &sc));

    // Register the window class.
    WNDCLASS wc = {};
    wc.lpfnWndProc = window_proc;
    wc.hInstance = hinstance;
    wc.hCursor = win32_check(LoadCursor(NULL, IDC_ARROW));
    wc.lpszClassName = TEXT("D3DWindow");
    win32_check(RegisterClass(&wc));

    // Create the window. We can use WS_EX_NOREDIRECTIONBITMAP
    // since all our presentation is happening through DirectComposition.
    HWND hwnd = win32_check(CreateWindowEx(
        WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, TEXT("D3D Window"),
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hinstance, NULL));

    // Bind our swap chain to the window.
    // TODO: Determine what DCompositionCreateDevice(NULL, ...) actually does.
    // I assume it creates a minimal IDCompositionDevice for use with D3D that can't actually
    // do any adapter-specific resource allocations itself, but I'm yet to verify this.
    IDCompositionDevice* dcomp;
    IDCompositionTarget* target;
    IDCompositionVisual* visual;
    hr_check(DCompositionCreateDevice(NULL, IID_PPV_ARGS(&dcomp)));
    hr_check(dcomp->CreateTargetForHwnd(hwnd, FALSE, &target));
    hr_check(dcomp->CreateVisual(&visual));
    hr_check(target->SetRoot(visual));
    hr_check(visual->SetContent(sc));
    hr_check(dcomp->Commit());

    // Show the window and enter the message loop.
    ShowWindow(hwnd, SW_SHOWNORMAL);
    while (true)
    {
        MSG msg;
        win32_check(GetMessage(&msg, NULL, 0, 0) > 0);
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM