【问题标题】:Smooth window resizing in Windows (using Direct2D 1.1)?在 Windows 中平滑调整窗口大小(使用 Direct2D 1.1)?
【发布时间】:2014-03-15 23:54:15
【问题描述】:

让我烦恼的是,Windows 中窗口的大小调整并不像我希望的那样“平滑”(Windows 程序一般都是这种情况,而不仅仅是我自己的。Visual Studio 就是一个很好的例子)。它使操作系统及其程序感觉“脆弱”和“便宜”(是的,我关心程序和用户界面的感觉,就像我关心关闭汽车的声音和感觉一样门。这是构建质量的反映),在我看来,这会影响整体用户体验,并最终影响品牌的认知度。

窗口内容的重绘根本跟不上调整大小期间的鼠标移动。每当我调整窗口大小时,都会出现“断断续续”/“闪烁”的效果,这似乎是由于在绘制新的、调整大小的内容之前在新的、调整大小的窗口框架中重绘了窗口的先前大小内容。

我正在构建一个使用 Direct2D 1.1 绘制其 UI 的 Win32 应用程序 (x64),鉴于 Direct2D 的速度,我认为在 2014 年的操作系统中应该没有必要遭受此类伪影。我自己在 Windows 8.1 上,但针对此应用程序的 Windows 7 及更高版本。

在最大化小窗口时,“以前的大小”效果特别明显(因为窗口大小的差异足够大,可以轻松对比旧内容的图像,因为它在较大窗口的左上角短暂闪烁随后在其上绘制新内容)。

这似乎是正在发生的事情:

  1. (假设屏幕上有一个完全渲染的窗口,大小为 500 x 500 像素)。
  2. 我最大化窗口:
  3. 窗口框架最大化
  4. 旧的 500 x 500 内容在新框架中绘制,之前..
  5. ..用适当大小的内容重新绘制最大化的窗口。

我想知道是否有任何方法可以缓解这种情况(即摆脱第 4 步) - 例如通过拦截 Windows 消息 - 并避免在最终重新绘制之前使用旧内容以新大小重新绘制窗口- 新内容的渲染发生。就像 Windows 自己重绘窗口一样,使用它已经可用的任何图形,在它费心要求我提供带有 WM_PAINT 消息或类似消息的更新内容之前。

可以吗?

编辑:似乎 WM_WINDOWPOSCHANGING / WM_SIZING 提供了对新尺寸数据的“早期访问”,但我仍然没有设法压制旧的绘画内容。

我的WndProc 看起来像这样:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_ERASEBKGND:
        return 1;
    case WM_PAINT:
        PAINTSTRUCT ps;
        BeginPaint(hWnd, &ps);
        D2DRender();
        EndPaint(hWnd, &ps);
        return 0;
    case WM_SIZE:
        if (DeviceContext && wParam != SIZE_MINIMIZED)
        {
            D2DResizeTargetBitmap();
            D2DRender();
        }
        return 0;
    case WM_DISPLAYCHANGE:
        D2DRender();
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

该窗口没有设置CS_HREDRAWCS_VREDRAW。交换链是双缓冲的,Present 调用是使用 SyncInterval = 0 进行的。

我知道,与在静态窗口表面上的普通重绘相比,每次窗口大小更改时重新创建交换链缓冲区确实会产生 一些 开销。但是,“卡顿”并不是由此引起的,因为即使在禁用缓冲区大小调整并且现有窗口内容在窗口调整大小期间仅缩放 时也会发生这种情况(尽管 确实它可以更好地跟上鼠标的移动)。

【问题讨论】:

  • 问题在于,在 Windows 中,绘画消息是异步的——当您开始处理重绘时,窗口可能已经再次改变了大小。您可以通过大量努力将影响最小化,但您永远无法完全克服它。
  • 我们是在谈论删除窗口边框并在客户区手动实现拖动/调整大小检测、自己确定新窗口大小、调整交换链大小、重新绘制然后在处理下一个(当前)鼠标之前手动调整窗口大小位置?
  • 我怀疑这些是你需要达到的长度。
  • @dialer:我理解响应性问题,但我在全屏窗口中以 60 FPS(无论如何都是屏幕刷新率)的速度折腾图形,所以我不明白为什么会这样在调整大小期间,重绘速度也不能用于以 60 FPS 更新窗口内容..
  • @dialer:当然。我怀疑这可能是早期操作系统版本的某种架构遗留物,最初是为较慢的处理器和 Direct2D / GPU 加速之前设计的。但这是 2014 年,我希望我的应用程序感觉像梅赛德斯,而不是 70 年代的斯柯达 :)

标签: c++ windows winapi paint direct2d


【解决方案1】:

如果您有一个固定大小(与全屏分辨率相同)的无边框子窗口(仅在父级内部呈现的类型),您应该会得到更平滑的结果,因为没有内存重新分配(我认为这是导致紧张)。

如果仍然不完美,请查看 WM_SIZE 和 WM_SIZING 并检查您是否可以对它们施展魔法。例如,在 WM_SIZING 上,您可以返回 true 告诉 Windows 您处理了消息(使窗口保持原样),然后您将 UI 重新渲染到具有 WM_SIZING 提供的大小的缓冲区,完成后,您发送自己的 WM_SIZING 但是使用 WPARAM 中的未使用位(连同其先前的内容),告诉您您有一个预渲染的缓冲区,您可以将其删除。从 msdn 上的 WM_SIZING 文档看来,WPARAM 应该有一些位可供您使用。

希望这会有所帮助。

【讨论】:

  • 该应用程序已经在内部运行了一个功能齐全的自定义(基于 Direct2D/3D)布局系统 - 类似于带有可以移动、调整大小、褪色等窗口的 OS 桌面 - 并且像丝绸一样光滑.它旨在以全屏模式运行。但是当应用程序在窗口模式下运行时,有理由将其保持为常规的窗口窗口 - 这就是挑战所在。
  • 我还没有尝试绕过所有基于 WM_ 的大小调整路径,而是依靠我自己的布局系统来处理它 - 通过拥有一个无边框的主窗口,但使用捕获鼠标和跟踪的“自定义”边框拖着。然后它将首先调整窗口缓冲区的大小(使用已调整大小的交换链缓冲区),重新渲染内容,然后手动设置窗口大小/位置以反映更改。而不是 Windows 首先调整窗口边框的大小,然后通过 WM_SIZE 等让我知道新的大小之后。这种断开可能是导致闪烁的原因。
  • 绕过 WM_size 消息听起来是个好主意,当我的项目进一步发展时,我也在寻找一种平滑的 Direct3D/OpenGL 调整大小的解决方案,也使用无边框窗口。请在您解决问题时发布您的解决方案,我也会这样做。
【解决方案2】:

调用CreateSwapChainForHwnd 时,请确保已将交换链描述Scaling 属性设置为DXGI_SCALING_NONE。这仅在具有平台更新的 Windows 7 上受支持,因此您可能需要回退到默认的 DXGI_SCALING_STRETCH(后者是导致闪烁的原因)。

【讨论】:

    【解决方案3】:

    有一种方法可以防止上面第 4 步中提到的不必要的 BitBlt。

    在 Windows 8 之前,可以通过创建您自己的 WM_NCCALCSIZE 的自定义实现来告诉 Windows 不进行任何传输(或在自身顶部传输一个像素),或者您也可以拦截 WM_WINDOWPOSCHANGING(第一次通过它到DefWindowProc) 并设置WINDOWPOS.flags |= SWP_NOCOPYBITS,这将禁用Windows 在调整窗口大小期间对SetWindowPos() 的内部调用中的BitBlt。这与跳过BitBlt 具有相同的最终效果。

    然而,没有什么可以这么简单。随着 Windows 8/10 Aero 的出现,应用程序现在绘制到屏幕外缓冲区,然后由新的、邪恶的 DWM.exe 窗口管理器合成。事实证明,DWM.exe 有时会在旧版 XP/Vista/7 代码已经完成的操作之上执行自己的 BitBlt 类型操作。阻止 DWM 执行 blit 要困难得多。到目前为止,我还没有看到任何完整的解决方案。

    所以你需要通过这两层。将突破XP/Vista/7层,至少提升8/10层性能的示例代码见:

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

    【讨论】:

      【解决方案4】:

      将 WM_SETREDRAW 设置为 FALSE,调整大小,然后重新启用绘图,使窗口无效,操作系统会对其进行 blit。

      我已经为从列表中选择不同项目时的按钮启用和禁用按钮做了此操作,而不是整个窗口。

      【讨论】:

      • 这完全禁用了绘画,因此在调整大小期间窗口内容根本不是动态的,而是简单地用黑色裁剪或扩展旧内容(分别取决于您是缩小还是放大) )。最重要的是,在操作结束时重新启用和重新绘制时,“绘制最后一个内容”工件仍然存在。
      【解决方案5】:

      这是我想出的最好的并且调整大小的效果很好,尽管后缓冲块传输会导致一些边缘闪烁,尚未使用 DX 或 OGL 进行测试,但它应该在硬件加速下工作得更好。它有点笨重,但可以作为概念证明。

      如果可以在不使用 MDI 的情况下裁剪画布,那就更好了,比如使用位掩码缓冲区。

      我不满意的一件事是子窗口的位置坐标,因为它们可能不适用于所有系统,但是 GetSystemMetrics 调用组合来获取边框和标题大小应该可以解决这个问题。

      /* Smooth resizing of GDI+ MDI window
       * 
       * Click window to resize, hit Escape or Alt+F4 to quit
       * 
       * Character type is set to multibyte
       * Project->Properties->Config Properties->General->Character Set = Multibyte
       * 
       * Pritam 2014 */
      
      
      // Includes
      #include <Windows.h>
      #include <gdiplus.h>
      #pragma comment (lib,"Gdiplus.lib")
      using namespace Gdiplus;
      
      
      // Max resolution
      #define XRES 1600
      #define YRES 900
      
      
      // Globals
      bool resizing = false;
      HWND parent, child;        // child is the canvas window, parent provides clipping of child
      Bitmap * buffer;
      
      
      // Render
      void Render() {
      
          // Get parent client size
          RECT rc;
          GetClientRect(parent, &rc);
      
          // Draw backbuffer
          Graphics * g = Graphics::FromImage(buffer);
      
              // Clear buffer
              g->Clear(Color(100, 100, 100));
      
              // Gray border
              Pen pen(Color(255, 180, 180, 180));
              g->DrawRectangle(&pen, 10, 10, rc.right - 20, rc.bottom - 20);
              pen.SetColor(Color(255, 0, 0, 0));
              g->DrawRectangle(&pen, 0, 0, rc.right - 1, rc.bottom - 1);
      
          // Draw buffer to screen
          PAINTSTRUCT ps;
          HDC hdc = BeginPaint(child, &ps);
          Graphics graphics(hdc);
      
              graphics.DrawImage(buffer, Point(0, 0));
      
          // Free
          EndPaint(child, &ps);
      }
      
      
      // MDI Callback
      LRESULT CALLBACK MDICallback(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
          switch(message) {
          case WM_LBUTTONDOWN:
              resizing = true; // Start resizing
              return 0;
              break;
          case WM_KEYDOWN:
              if(wparam == VK_ESCAPE) { // Exit on escape
                  PostQuitMessage(0);
              }
              TranslateMessage((const MSG *)&message);
              return 0;
              break;
          case WM_PAINT:
              Render();
              return 0;
              break;
          case WM_DESTROY:
              PostQuitMessage(0);
              return 0;
              break;
          }
      
          return DefMDIChildProc(hwnd, message, wparam, lparam);
      }
      
      
      // Parent window callback
      LRESULT CALLBACK WndCallback(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
          return DefFrameProc(hwnd, child, message, wparam, lparam);
      }
      
      
      // Create windows
      bool CreateWindows(void) {
      
          // Parent class
          WNDCLASSEX wndclass;
          ZeroMemory(&wndclass, sizeof(wndclass)); wndclass.cbSize = sizeof(wndclass);
      
              wndclass.style = CS_NOCLOSE;
              wndclass.lpfnWndProc = WndCallback;
              wndclass.hInstance = GetModuleHandle(NULL);
              wndclass.lpszClassName = "WNDCALLBACKPARENT";
              wndclass.hIcon = LoadIcon(NULL, IDI_WINLOGO);
              wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
      
          if(!RegisterClassEx(&wndclass)) return false;
      
              // MDI class
              wndclass.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
              wndclass.lpfnWndProc = MDICallback;
              wndclass.lpszClassName = "MDICALLBACKCANVAS";
      
          if(!RegisterClassEx(&wndclass)) return false;
      
      
          // Parent window styles
          DWORD style = WS_POPUP | WS_CLIPCHILDREN;
          DWORD exstyle = 0;
      
              // Set initial window size and position
              RECT rc;
              rc.right = 640;
              rc.bottom = 480;
      
              AdjustWindowRectEx(&rc, style, false, exstyle);
      
              rc.left = 20;
              rc.top = 20;
      
          // Create window
          if(!(parent = CreateWindowEx(exstyle, "MDICLIENT", "MDI Resize", style, rc.left, rc.top, rc.right, rc.bottom, NULL, NULL, wndclass.hInstance, NULL))) return false;
      
      
          // MDI window styles
          style = MDIS_ALLCHILDSTYLES;
          exstyle = WS_EX_MDICHILD;
      
              // Set MDI size
              rc.left = - 8; // The sizes occupied by borders and caption, if position is not correctly set an ugly caption will appear
              rc.top = - 30;
              rc.right = XRES;
              rc.bottom = YRES;
              AdjustWindowRectEx(&rc, style, false, exstyle);
      
          // Create MDI child window
          if(!(child = CreateWindowEx(exstyle, "MDICALLBACKCANVAS", "", style, rc.left, rc.top, rc.right, rc.bottom, parent, NULL, wndclass.hInstance, NULL))) return 8;
      
              // Finalize
              ShowWindow(child, SW_SHOW);
              ShowWindow(parent, SW_SHOWNORMAL);
      
          // Success
          return true;
      }
      
      
      // Resize
      void Resize(void) {
      
          // Init
          RECT rc, rcmdi;
          GetClientRect(child, &rcmdi); // Use mdi window size to set max resize for parent
          GetWindowRect(parent, &rc);
      
          // Get mouse position
          POINT mp;
          GetCursorPos(&mp);
      
              // Set new size
              rc.right = mp.x - rc.left + 10;
              rc.bottom = mp.y - rc.top + 10;
      
              // Apply min & max size
              if(rc.right < 240) rc.right = 240; if(rc.bottom < 180) rc.bottom = 180;
              if(rc.right > rcmdi.right) rc.right = rcmdi.right; if(rc.bottom > rcmdi.bottom) rc.bottom = rcmdi.bottom;
      
          // Update window size
          SetWindowPos(parent, NULL, rc.left, rc.top, rc.right, rc.bottom, SWP_NOZORDER | SWP_NOMOVE);
      
              // Make sure client is entirely repainted
              GetClientRect(child, &rc);
              InvalidateRect(child, &rc, false);
              UpdateWindow(child);
      
          // Stop resizing if mousebutton is up
          if(!(GetKeyState(VK_LBUTTON) & 1 << (sizeof(short) * 8 - 1)))
              resizing = false;
      }
      
      
      // Main
      int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE pinstance, LPSTR cmdline, int cmdshow) {
      
          // Initiate GDI+
          ULONG_PTR gdiplusToken;
          GdiplusStartupInput gdiplusStartupInput;
          GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
      
          buffer = new Bitmap(XRES, YRES, PixelFormat24bppRGB);
      
          // Create windows
          if(!CreateWindows()) return 1;
      
      
          // Main loop
          bool running = true;
          MSG message;
          while(running) {
      
              // Check message or pass them on to window callback
              if(PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
                  if(message.message == WM_QUIT) {
                      running = false;
                  } else {
                      if(!TranslateMDISysAccel(child, &message)) {
                          TranslateMessage(&message);
                          DispatchMessage(&message);
                      }
                  }
              }
      
              // Resize
              if(resizing)
                  Resize();
      
              // Sleep a millisecond to spare the CPU
              Sleep(1);
          }
      
      
          // Free memmory and exit
          delete buffer;
          GdiplusShutdown(gdiplusToken);
          return 0;
      }
      

      编辑:另一个使用“位掩码”/分层窗口的示例。

      // Escape to quit, left mousebutton to move window, right mousebutton to resize.
      // And again char set must be multibyte
      
      // Include
      #include <Windows.h>
      #include <gdiplus.h>
      #pragma comment (lib,"Gdiplus.lib")
      using namespace Gdiplus;
      
      
      // Globals
      Bitmap * backbuffer;
      int xres, yres;
      bool move, size;
      POINT framePos, frameSize, mouseOffset;
      
      // Renders the backbuffer
      void Render(void) {
          if(!backbuffer) return;
      
          // Clear window with mask color
          Graphics * gfx = Graphics::FromImage(backbuffer);
          gfx->Clear(Color(255, 0, 255));
      
          // Draw stuff
          SolidBrush brush(Color(120, 120, 120));
          gfx->FillRectangle(&brush, framePos.x, framePos.y, frameSize.x, frameSize.y);
      }
      
      // Paints the backbuffer to window
      void Paint(HWND hwnd) {
          if(!hwnd) return;
          PAINTSTRUCT ps;
          HDC hdc = BeginPaint(hwnd, &ps);
          Graphics gfx(hdc);
          gfx.DrawImage(backbuffer, Point(0, 0));
          EndPaint(hwnd, &ps);
      }
      
      
      void HandleMove(HWND hwnd) {
      
          // Get mouse position
          POINT mouse;
          GetCursorPos(&mouse);
      
          // Update frame position
          framePos.x = mouse.x - mouseOffset.x;
          framePos.y = mouse.y - mouseOffset.y;
      
          // Redraw buffer and invalidate & update window
          Render();
          InvalidateRect(hwnd, NULL, false);
          UpdateWindow(hwnd);
      
          // Stop move
          if(!(GetKeyState(VK_LBUTTON) & 1 << (sizeof(short) * 8 - 1)))
              move = false;
      }
      
      void HandleSize(HWND hwnd) {
      
          // Get mouse position
          POINT mouse;
          GetCursorPos(&mouse);
      
          // Update frame size
          frameSize.x = mouse.x + mouseOffset.x - framePos.x;
          frameSize.y = mouse.y + mouseOffset.y - framePos.y;
      
          //frameSize.x = mouse.x + mouseOffset.x;
          //frameSize.y = mouse.y + mouseOffset.y;
      
          // Redraw buffer and invalidate & update window
          Render();
          InvalidateRect(hwnd, NULL, false);
          UpdateWindow(hwnd);
      
          // Stop size
          if(!(GetKeyState(VK_RBUTTON) & 1 << (sizeof(short) * 8 - 1)))
              size = false;
      }
      
      
      LRESULT CALLBACK WindowCallback(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
      
          POINTS p;
      
          switch(msg) {
          case WM_KEYDOWN:
              if(wparam == VK_ESCAPE) PostQuitMessage(0);
              return 0;
              break;
          case WM_LBUTTONDOWN:
              p = MAKEPOINTS(lparam); // Get mouse coords
              mouseOffset.x = p.x - framePos.x;
              mouseOffset.y = p.y - framePos.y;
              move = true;
              break;
          case WM_RBUTTONDOWN:
              p = MAKEPOINTS(lparam);
              mouseOffset.x = framePos.x + frameSize.x - p.x;
              mouseOffset.y = framePos.y + frameSize.y - p.y;
              size = true;
              break;
          case WM_PAINT:
              Paint(hwnd);
              return 0;
              break;
          case WM_DESTROY:
              PostQuitMessage(0);
              return 0;
              break;
          }
          return DefWindowProc(hwnd, msg, wparam, lparam);
      }
      
      
      // Main
      int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE pinstance, LPSTR cmdline, int cmdshow) {
      
          // Init resolution, frame
          xres = GetSystemMetrics(SM_CXSCREEN);
          yres = GetSystemMetrics(SM_CYSCREEN);
      
          move = false; size = false;
          framePos.x = 100; framePos.y = 80;
          frameSize.x = 320; frameSize.y = 240;
          mouseOffset.x = 0; mouseOffset.y = 0;
      
          // Initiate GDI+
          ULONG_PTR gdiplusToken;
          GdiplusStartupInput gdiplusStartupInput;
          GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
      
          // Init backbuffer
          backbuffer = ::new Bitmap(xres, yres, PixelFormat24bppRGB);
          Render();
      
      
          // Window class
          WNDCLASSEX wc; ZeroMemory(&wc, sizeof(wc)); wc.cbSize = sizeof(wc);
      
          wc.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
          wc.lpfnWndProc = WindowCallback;
          wc.hInstance = GetModuleHandle(NULL);
          wc.lpszClassName = "SingleResizeCLASS";
          wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
          wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
          wc.hCursor = LoadCursor(NULL, IDC_ARROW);
      
          if(!RegisterClassEx(&wc)) return 1;
      
      
          // Create window
          HWND hwnd;
          DWORD style = WS_POPUP;
          DWORD exstyle = WS_EX_LAYERED;
          if(!(hwnd = CreateWindowEx(exstyle, wc.lpszClassName, "Resize", style, 0, 0, xres, yres, NULL, NULL, wc.hInstance, NULL)))
              return 2;
      
              // Make window fully transparent to avoid the display of unpainted window
              SetLayeredWindowAttributes(hwnd, 0, 0, LWA_ALPHA);
      
          // Finalize
          ShowWindow(hwnd, SW_SHOWNORMAL);
          UpdateWindow(hwnd);
      
          // Make window fully opaque, and set color mask key
          SetLayeredWindowAttributes(hwnd, RGB(255, 0, 255), 0, LWA_COLORKEY);
      
      
          // Main loop
          MSG msg;
          bool running = true;
          while(running) {
      
              // Check message
              if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
                  if(msg.message == WM_QUIT) {
                      running = false;
                  } else {
                      TranslateMessage(&msg);
                      DispatchMessage(&msg);
                  }
              }
      
              // Move or size frame
              if(move) { HandleMove(hwnd); }
              if(size) { HandleSize(hwnd); }
      
              Sleep(1);
          }
      
          // Free memory
          ::delete backbuffer;
          backbuffer = NULL;
          GdiplusShutdown(gdiplusToken);
      
          // Exit
          return 0;
      }
      

      【讨论】:

      • 我认为你最好的选择是像 this
      【解决方案6】:

      虽然您的目标值得称赞,但我怀疑任何这样做的尝试都会以您和 Windows 之间的斗争而告终——您不会赢(尽管您可能会设法以自己的方式争取获得光荣的平局)。很抱歉我是负面的。

      【讨论】:

        猜你喜欢
        • 2019-04-30
        • 1970-01-01
        • 1970-01-01
        • 2011-10-31
        • 1970-01-01
        • 1970-01-01
        • 2021-09-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多