无闪烁主窗口调整大小






1.75/5 (16投票s)
无闪烁地调整主窗口大小。
问题
Windows 中的所有框架窗口在调整大小的时候都会闪烁,尤其是在从顶部/左上角调整的时候。
原因
当窗口大小在调整过程中增大时,Windows 会自动在窗口的左上部分绘制旧的内容,然后发送 WM_WINDOWPOSCHANGED
消息给窗口,这最终会向所有子窗口发送 WM_SIZE
消息,以及许多其他需要一些时间处理的消息。只有一段时间后,旧的内容才会被某个客户端窗口擦除。
因此,例如,在调整框架窗口大小时,状态栏会感觉上下跳动。
解决方案
类 CMainFrameResize
包含在调整大小期间的框架窗口的屏幕截图。在收到 WM_WINDOWPOSCHANGED
消息后,它会**将旧窗口的内容拉伸到新窗口上**(使用 StretchBlt
,因为新窗口的大小可能大于或小于捕获的窗口)。 这样,就产生了一种立即调整大小的错觉,并且在视觉上几乎没有闪烁。 这是实现此功能的关键代码:
LRESULT CMainFrameResize::OnWindowPosChanged(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
LRESULT ret;
CRect rcWnd;
m_pWnd->GetWindowRect(&rcWnd);
ret = 0;
if(rcWnd.Size() != m_rcWnd.Size())
{
if(m_rcCapture == CRect(0, 0, 0, 0)) // capture for the first time
CaptureWindow();
// first of all stretch the previous captured image to
// have something to show during the following lengthy operation
{
CWindowDC dcWnd(m_pWnd);
dcWnd.StretchBlt(0, 0, rcWnd.Width(), rcWnd.Height(),
&m_dcCapture, 0, 0, m_rcCapture.Width(),
m_rcCapture.Height(), SRCCOPY);
}
// now wm_size is sent to all children, a lengthy operation
m_pWnd->SetRedraw(FALSE);
ret = CallWindowProc((WNDPROC)m_hPrevProc, hwnd, uMsg, wParam, lParam);
m_pWnd->SetRedraw(TRUE);
// now get the new contents
CaptureWindow();
// draw the new contents in one blit
{
CWindowDC dcWnd(m_pWnd);
dcWnd.BitBlt(0, 0, rcWnd.Width(), rcWnd.Height(),
&m_dcCapture, 0, 0, SRCCOPY);
}
// update m_rcWnd
m_rcWnd = rcWnd;
}
else
if(!m_bResizing)
ret = CallWindowProc((WNDPROC)m_hPrevProc, hwnd, uMsg, wParam, lParam);
return ret;
}
void CMainFrameResize::CaptureWindow()
{
// use PrintWindow to capture the window to our dc
m_pWnd->GetWindowRect(&m_rcCapture);
m_pWnd->PrintWindow(&m_dcCapture, 0);
}
使用代码
在 CMainFrame
中包含以下变量
#include "MainFrmResize.h"
class CMainFrame : public CFrameWnd
{
...
CMainFrameResize m_resize;
}
并在 CMainFrame::OnCreate
内部
m_resize.Attach(this);
CS_HREDRAW 和 CS_VREDRAW
这两个窗口类样式使窗口在调整大小期间即使在主框架窗口收到 SetRedraw(FALSE)
后也会重新绘制自身。它们也负责调整大小期间的一些闪烁。 因此,必须从应用程序中的所有窗口中删除它们。
您可以在自定义的 CWnd
派生类中创建自己的窗口,并重写 PreCreateWindow
,以确保它们不在 AfxRegisterWndClass
中传递 CS_HREDRAW
和 CS_VREDRAW
。 问题在于需要从现有的类(如 CToolBar
和 CStatusBar
)中删除这两个类样式。 这里有一个模板类可以做到这一点
template<class BaseClass>
class CWndNoCSHVRedraw : public BaseClass
{
public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs)
{
WNDCLASSEX wc;
ATOM atmRegister;
if(GetClassInfoEx(NULL, cs.lpszClass, &wc))
{
if(wc.style & (CS_HREDRAW | CS_VREDRAW))
{
wc.cbSize = sizeof(wc);
CString strClassNew;
strClassNew.Format(_T("%sNOCSREDRAW"), wc.lpszClassName);
wc.lpszClassName = strClassNew;
wc.style &= ~(CS_HREDRAW | CS_VREDRAW);
atmRegister = RegisterClassEx(&wc);
ASSERT(atmRegister);
cs.lpszClass = (LPCTSTR)atmRegister;
}
}
else
cs.lpszClass = AfxRegisterWndClass(CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW),
(HBRUSH) ::GetStockObject(WHITE_BRUSH),
::LoadIcon(NULL, IDI_APPLICATION));
cs.dwExStyle &= ~WS_EX_CLIENTEDGE;
cs.style |= WS_CLIPCHILDREN;
if(!BaseClass::PreCreateWindow(cs))
return FALSE;
return TRUE;
};
};
在示例应用程序中,在 MainFrm.h 内部,我们编写
CWndNoCSHVRedraw<CStatusBar> m_wndStatusBar;
CWndNoCSHVRedraw<CToolBar> m_wndToolBar;
并且我们也从该类派生视图(而不是直接从 CView
派生)
class CTestJitterView : public CWndNoCSHVRedraw<CView>
并在 CTestJitterView::PreCreateWindow
中(或者我们可以完全删除重写的 PreCreateWindow
函数)
return CWndNoCSHVRedraw<CView>::PreCreateWindow(cs);