Как заставить окна НЕ перерисовывать что-либо в моем диалоге, когда пользователь изменяет размер моего диалога?
Когда пользователь захватывает угол окна с изменяемым размером, а затем перемещает его, Windows сначала перемещает содержимое окна, а затем выдает WM_SIZE для окна, размер которого изменяется.
Таким образом, в диалоговом окне, в котором я хочу управлять перемещением различных дочерних элементов управления и хочу устранить мерцание, пользователь сначала видит, как ОС Windows думает, что окно будет выглядеть (потому что, AFAICT, ОС использует битовый подход к перемещению что-то внутри окна перед отправкой WM_SIZE) - и только тогда мой диалог получает возможность перемещать свои дочерние элементы управления или изменять их размер и т. д., после чего он должен заставить объекты перерисовываться, что теперь вызывает мерцание (в самом наименее).
Мой главный вопрос: есть ли способ заставить Windows НЕ делать эту глупую штуку? Это определенно будет неправильно в случае окна с элементами управления, которые перемещаются по мере изменения размера окна или сами по себе при изменении размера родительского элемента. В любом случае, предварительная покраска операционной системы просто приводит в порядок работы.
Некоторое время я думал, что это может быть связано с флагами классов CS_HREDRAW и CSVREDRAW. Однако реальность такова, что я не хочу, чтобы ОС попросила меня стереть окно - я просто хочу перерисовать себя без ОС, предварительно изменив содержимое моего окна (т.е. я хочу, чтобы дисплей был таким, какой он был до того, как пользователь начал изменять размер - без какого-либо побитового удара из ОС). И я не хочу, чтобы ОС сообщала каждому элементу управления о том, что ее тоже нужно перерисовать (если только она не была фактически скрыта или обнаружена изменением размера).
Что я действительно хочу:
- Для перемещения и изменения размера дочерних элементов управления, прежде чем что-либо будет обновлено на экране.
- Полностью нарисуйте все перемещенные или измененные дочерние элементы управления, чтобы они отображались без артефактов в новом размере и местоположении.
- Нарисуйте промежутки между дочерними элементами управления, не затрагивая сами дочерние элементы управления.
ПРИМЕЧАНИЕ. Шаги 2 и 3 можно поменять местами.
Вышеупомянутые три вещи, кажется, происходят правильно, когда я использую DeferSetWindowPos() в сочетании с ресурсом диалога, отмеченным как WS_CLIPCHILDREN.
Я получил бы дополнительное небольшое преимущество, если бы мог сделать вышеупомянутое с DC памяти, а затем сделать только один битовый бит в конце обработчика WM_SIZE.
Я играл с этим некоторое время, и я не могу избежать двух вещей:
Я до сих пор не могу запретить Windows делать "предсказательный битблт". Ответ: См. Ниже решение, которое переопределяет WM_NCCALCSIZE, чтобы отключить это поведение.
Я не вижу, как можно построить диалог, в котором его дочерние элементы управления рисуют в двойной буфер. Ответ: См. Ответ Джона (помеченный как ответ) ниже, чтобы узнать, как попросить ОС Windows дважды буферизовать ваш диалог (примечание: в соответствии с документами это запрещает любые промежуточные операции рисования GetDC()).
Мое окончательное решение (спасибо всем, кто внес свой вклад, особенно Джон К.):
После долгих потов и слез я обнаружил, что следующая техника работает безупречно, как в Aero, так и в XP или с отключенным Aero. Flicking не существует (1).
- Зацепи диалог проц.
- Переопределите WM_NCCALCSIZE, чтобы заставить Windows проверять всю клиентскую область, а не что-либо делать.
- Переопределите WM_SIZE, чтобы выполнить все ваши перемещения и изменения размеров, используя BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos для всех видимых окон.
- Убедитесь, что диалоговое окно имеет стиль WS_CLIPCHILDREN.
- НЕ используйте CS_HREDRAW|CS_VREDRAW (диалоги нет, поэтому, как правило, не проблема).
Код макета зависит от вас - достаточно просто найти примеры в CodeGuru или CodeProject менеджеров компоновки или создать свой собственный.
Вот некоторые выдержки из кода, которые помогут вам в большинстве случаев:
LRESULT ResizeManager::WinProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg)
{
case WM_ENTERSIZEMOVE:
m_bResizeOrMove = true;
break;
case WM_NCCALCSIZE:
// The WM_NCCALCSIZE idea was given to me by John Knoeller:
// see: http://stackru.com/questions/2165759/how-do-i-force-windows-not-to-redraw-anything-in-my-dialog-when-the-user-is-resiz
//
// The default implementation is to simply return zero (0).
//
// The MSDN docs indicate that this causes Windows to automatically move all of the child controls to follow the client's origin
// and experience shows that it bitblts the window's contents before we get a WM_SIZE.
// Hence, our child controls have been moved, everything has been painted at its new position, then we get a WM_SIZE.
//
// Instead, we calculate the correct client rect for our new size or position, and simply tell windows to preserve this (don't repaint it)
// and then we execute a new layout of our child controls during the WM_SIZE handler, using DeferWindowPos to ensure that everything
// is moved, sized, and drawn in one go, minimizing any potential flicker (it has to be drawn once, over the top at its new layout, at a minimum).
//
// It is important to note that we must move all controls. We short-circuit the normal Windows logic that moves our child controls for us.
//
// Other notes:
// Simply zeroing out the source and destination client rectangles (rgrc[1] and rgrc[2]) simply causes Windows
// to invalidate the entire client area, exacerbating the flicker problem.
//
// If we return anything but zero (0), we absolutely must have set up rgrc[0] to be the correct client rect for the new size / location
// otherwise Windows sees our client rect as being equal to our proposed window rect, and from that point forward we're missing our non-client frame
// only override this if we're handling a resize or move (I am currently unaware of how to distinguish between them)
// though it may be adequate to test for wparam != 0, as we are
if (bool bCalcValidRects = wparam && m_bResizeOrMove)
{
NCCALCSIZE_PARAMS * nccs_params = (NCCALCSIZE_PARAMS *)lparam;
// ask the base implementation to compute the client coordinates from the window coordinates (destination rect)
m_ResizeHook.BaseProc(hwnd, msg, FALSE, (LPARAM)&nccs_params->rgrc[0]);
// make the source & target the same (don't bitblt anything)
// NOTE: we need the target to be the entire new client rectangle, because we want windows to perceive it as being valid (not in need of painting)
nccs_params->rgrc[1] = nccs_params->rgrc[2];
// we need to ensure that we tell windows to preserve the client area we specified
// if I read the docs correctly, then no bitblt should occur (at the very least, its a benign bitblt since it is from/to the same place)
return WVR_ALIGNLEFT|WVR_ALIGNTOP;
}
break;
case WM_SIZE:
ASSERT(m_bResizeOrMove);
Resize(hwnd, LOWORD(lparam), HIWORD(lparam));
break;
case WM_EXITSIZEMOVE:
m_bResizeOrMove = false;
break;
}
return m_ResizeHook.BaseProc(hwnd, msg, wparam, lparam);
}
Изменение размера действительно выполняется членом Resize() следующим образом:
// execute the resizing of all controls
void ResizeManager::Resize(HWND hwnd, long cx, long cy)
{
// defer the moves & resizes for all visible controls
HDWP hdwp = BeginDeferWindowPos(m_resizables.size());
ASSERT(hdwp);
// reposition everything without doing any drawing!
for (ResizeAgentVector::const_iterator it = m_resizables.begin(), end = m_resizables.end(); it != end; ++it)
VERIFY(hdwp == it->Reposition(hdwp, cx, cy));
// now, do all of the moves & resizes at once
VERIFY(EndDeferWindowPos(hdwp));
}
И, возможно, последний хитрый момент можно увидеть в обработчике ResizeAgent's Reposition():
HDWP ResizeManager::ResizeAgent::Reposition(HDWP hdwp, long cx, long cy) const
{
// can't very well move things that no longer exist
if (!IsWindow(hwndControl))
return hdwp;
// calculate our new rect
const long left = IsFloatLeft() ? cx - offset.left : offset.left;
const long right = IsFloatRight() ? cx - offset.right : offset.right;
const long top = IsFloatTop() ? cy - offset.top : offset.top;
const long bottom = IsFloatBottom() ? cy - offset.bottom : offset.bottom;
// compute height & width
const long width = right - left;
const long height = bottom - top;
// we can defer it only if it is visible
if (IsWindowVisible(hwndControl))
return ::DeferWindowPos(hdwp, hwndControl, NULL, left, top, width, height, SWP_NOZORDER|SWP_NOACTIVATE);
// do it immediately for an invisible window
MoveWindow(hwndControl, left, top, width, height, FALSE);
// indicate that the defer operation should still be valid
return hdwp;
}
Хитрость заключается в том, что мы стараемся не пытаться связываться с любыми окнами, которые были разрушены, и мы не пытаемся отложить SetWindowPos против невидимого окна (так как это задокументировано как "потерпит неудачу").
Я протестировал вышесказанное в реальном проекте, который скрывает некоторые элементы управления и использует довольно сложные макеты с отличным успехом. Нет мерцания (1) даже без Aero, даже если вы изменяете размер, используя верхний левый угол диалогового окна (большинство окон с изменяемым размером будут отображать наибольшее количество мерцаний и проблем при захвате этой ручки - IE, FireFox и т. Д.).
Если будет достаточно интереса, меня могут убедить отредактировать мои выводы с помощью реального примера реализации для CodeProject.com или где-то подобного. Сообщи мне.
(1) Обратите внимание, что невозможно избежать одного розыгрыша поверх того, что было раньше. Для каждой части диалогового окна, которая не изменилась, пользователь ничего не видит (никакого мерцания). Но там, где что-то изменилось, есть изменения, видимые для пользователя - этого невозможно избежать, и это 100% решение.
8 ответов
Вы не можете предотвратить рисование во время изменения размера, но вы можете (с осторожностью) предотвратить перекрашивание, отсюда и мерцание. во-первых, битблт.
Есть два способа остановить битбит.
Если у вас есть класс окна верхнего уровня, просто зарегистрируйте его в CS_HREDRAW | CS_VREDRAW
стили. Это приведет к изменению размера вашего окна, чтобы сделать недействительной всю клиентскую область, вместо того, чтобы пытаться угадать, какие биты не будут меняться, и битблинг.
Если вы не являетесь владельцем класса, но у вас есть возможность контролировать обработку сообщений (верно для большинства диалоговых окон). Обработка по умолчанию WM_NCCALCSIZE
где стили классов CS_HREDRAW
а также CS_VREDRAW
обрабатываются, поведение по умолчанию является возвращение WVR_HREDRAW | WVR_VREDRAW
от обработки WM_NCCALCSIZE, когда класс CS_HREDRAW | CS_VREDRAW
,
Поэтому, если вы можете перехватить WM_NCCALCSIZE, вы можете принудительно вернуть эти значения после вызова DefWindowProc, чтобы выполнить другую обычную обработку.
Вы можете слушать WM_ENTERSIZEMOVE
а также WM_EXITSIZEMOVE
знать, когда изменение размера вашего окна запускается и останавливается, и использовать это для временного отключения или изменения способа работы вашего чертежа и / или кода макета для минимизации мигания. Что именно вы хотите сделать, чтобы изменить этот код, будет зависеть от того, что обычно делает ваш обычный код в WM_SIZE
WM_PAINT
а также WM_ERASEBKGND
,
Когда вы рисуете фон вашего диалогового окна, вам не нужно рисовать за дочерними окнами. Решив, что в диалоге есть WS_CLIPCHILDREN, вы решите это.
Когда вы перемещаете дочерние окна, убедитесь, что вы используете BeginDeferWindowPos / EndDefwindowPos, чтобы все перерисовки происходили одновременно. В противном случае вы получите кучу миганий, поскольку каждое окно перерисовывает свою неклиентскую область при каждом вызове SetWindowPos.
Если я правильно понял вопрос, это именно тот вопрос, к которому Раймонд обратился сегодня.
Вот обновление 2018 года, поскольку я только что пробежал через ту же самую перчатку, что и вы.
"Окончательное решение" в вашем вопросе и связанные с ним ответы, в которых упоминаются трюки с WM_NCCALCSIZE
а также CS_HREDRAW|CS_VREDRAW
хороши для предотвращения Windows XP/Vista/7 от выполнения BitBlt
что приставает к вашей клиентской области во время изменения размера. Может быть даже полезно упомянуть похожий трюк: вы можете перехватить WM_WINDOWPOSCHANGING
(сначала передавая его на DefWindowProc
) и установить WINDOWPOS.flags |= SWP_NOCOPYBITS
, который отключает BitBlt
внутри внутреннего звонка SetWindowPos()
что Windows делает во время изменения размера окна. Это имеет тот же возможный эффект пропуска BitBlt
,
И некоторые люди упоминали, что ваш WM_NCCALCSIZE
трюк больше не работает в Windows 10. Я думаю, что это может быть потому, что код, который вы написали, возвращает WVR_ALIGNLEFT|WVR_ALIGNTOP
когда он должен вернуться WVR_VALIDRECTS
для двух прямоугольников, которые вы построили (nccs_params->rgrc[1]
а также nccs_params->rgrc[2]
) для использования Windows, по крайней мере, в соответствии с очень скудным доксом на страницах MSDN для WM_NCCALCSIZE
а также NCCALCSIZE_PARAMS
, Возможно, что Windows 10 более строго относится к этому возвращаемому значению; Я бы попробовал это.
Тем не менее, даже если мы предположим, что мы можем убедить Windows 10 не делать BitBlt
внутри SetWindowPos()
Оказывается, есть новая проблема...
Windows 10 (и, возможно, также Windows 8) добавляет еще один уровень растления клиентской области поверх старого унаследованного из XP/Vista/7.
В Windows 10 приложения не рисуют непосредственно в кадровый буфер, а вместо этого рисуют в закадровые буферы, которые составляет Aero Window manager (DWM.exe).
Оказывается, что DWM иногда решает "помочь" вам, рисуя собственный контент над вашей клиентской областью (вроде как BitBlt
но еще более извращенный и даже дальше от вашего контроля).
Таким образом, чтобы быть свободным от растления клиентов, нам все еще нужно получить WM_NCCALCSIZE
под контролем, но мы также должны предотвратить порчу DWM с вашими пикселями.
Я боролся с точно такой же проблемой и создал сводку вопросов / ответов, в которой собраны 10-летние посты по этой теме и предложены некоторые новые идеи (слишком долго, чтобы вставить содержимое в этот вопрос). Упомянутый выше BitBlt больше не является единственной проблемой, как в Windows Vista. Наслаждаться:
Для некоторых элементов управления вы можете использовать сообщение WM_PRINT, чтобы преобразовать элемент в DC. Но это на самом деле не решает вашу основную проблему: вы хотите, чтобы Windows НЕ рисовала что-либо во время изменения размера, а позволяла вам делать все это.
И ответ таков: вы просто не можете делать то, что хотите, пока у вас есть дочерние окна.
То, как я в конечном итоге решил эту проблему в своем собственном коде, это переключение на использование элементов управления без окон. Поскольку у них нет собственного окна, они всегда рисуют в одно и то же время (и в том же DC), что и их родительское окно. Это позволяет мне использовать простую двойную буферизацию для полного устранения мерцания. Я даже могу тривиально подавить рисование детей, когда мне нужно просто не вызывать их процедуру рисования внутри процедуры рисования родителя.
Это единственный известный мне способ полностью избавиться от мерцания и разрыва во время операций изменения размера.
Существует только один способ эффективно диагностировать проблемы перекрашивания - удаленная отладка.
Получи второй ПК. Установите MSVSMON на нем. Добавьте шаг после сборки или служебный проект, который копирует ваши продукты сборки на удаленный ПК.
Теперь вы должны иметь возможность размещать точки останова в обработчиках WM_PAINT, обработчиках WM_SIZE и т. Д. И фактически отслеживать трассировочный код, когда он выполняет размер и перерисовку. Если вы загрузите символы с серверов символов MS, вы сможете увидеть полные стеки вызовов.
Некоторые хорошо расположенные точки останова - в ваших обработчиках WM_PAINT, WM_ERAGEBKGND, и вы должны иметь хорошее представление о том, почему ваше окно синхронно перерисовывается в начале цикла WM_SIZE.
В системе есть МНОЖЕСТВО окон, которые состоят из родительского окна с многоуровневыми дочерними элементами управления - окна проводника сильно усложняются представлениями списков, панелями предварительного просмотра древовидных структур и т. Д. В Проводнике нет проблем с мерцанием при изменении размера, поэтому вполне возможно получить его. Изменение размеров родительских окон без мерцания:- вам нужно перехватить перерисовки, выяснить, что их вызвало, и, в общем, убедиться, что причина устранена.
Что, кажется, работает:
- Используйте WS_CLIPCHILDREN в родительском диалоге (можно установить в WM_INITDIALOG)
- Во время WM_SIZE, проходите через дочерние элементы управления, перемещая и изменяя их размер, используя DeferSetWindowPos().
Это очень близко к идеалу, в моем тестировании под Windows 7 с Aero.
Если вы можете найти место для его подключения, CWnd::LockWindowUpdates()
будет препятствовать любому рисунку, пока вы не разблокируете обновления.
Но имейте в виду, что это взлом, и довольно уродливый в этом. Ваше окно будет выглядеть ужасно при изменении размеров. Если проблема, с которой вы сталкиваетесь, - это мерцание во время изменения размера, тогда лучше всего диагностировать мерцание, а не скрывать мерцание, блокируя краски.
Нужно искать команды перерисовки, которые вызываются слишком часто во время изменения размера. Если вы используете окно управления RedrawWindow()
с RDW_UPDATENOW
флаг указан, он будет перекрашиваться тогда и там. Но вы можете снять этот флаг и указать RDW_INVALIDATE
вместо этого, который говорит элементу управления, чтобы сделать недействительным окно без перерисовки. Он будет перекрашиваться во время простоя, сохраняя дисплей свежим без разбрызгивания.
Существуют различные подходы, но я обнаружил, что обычно можно использовать только двойную буферизацию: отрисовка за пределами экрана, затем перетаскивание всего буфера на экран.
Это бесплатно в Vista Aero и выше, так что ваша боль может быть недолгим.
Я не знаю об общей реализации двойной буферизации для окон и системных элементов управления под XP, однако вот некоторые вещи для изучения:
CMemDC Keith Rule для двойной буферизации всего, что вы рисуете с помощью GDI
WS_EX_COMPOSITED Стиль окна (см. Раздел " Примечания" и кое-что здесь о stackru)