Как заставить окна НЕ перерисовывать что-либо в моем диалоге, когда пользователь изменяет размер моего диалога? - PullRequest
32 голосов
/ 30 января 2010

Когда пользователь захватывает угол окна с изменяемым размером, а затем перемещает его, Windows сначала перемещает содержимое окна, а затем выдает WM_SIZE для изменяемого размера окна.

Таким образом, в диалогегде я хочу управлять движением различных дочерних элементов управления и хочу устранить мерцание, пользователь сначала видит, как ОС Windows думает, что окно будет выглядеть (потому что, AFAICT, ОС использует подход bitblt для перемещения объектов внутри окнаперед отправкой WM_SIZE) - и только , а затем мой диалог обрабатывает перемещение своих дочерних элементов управления или изменяет их размер и т. д., после чего он должен заставить объекты перерисовываться, что теперь вызывает мерцание (припо крайней мере).

Мой главный вопрос: Есть ли способ заставить окна НЕ делать эту глупую штуку с бит-битом? Это определенно будет неправильно в случае окна с элементами управленияэто изменение размера окна или изменение размера самих себя при изменении размера родительского элемента.В любом случае, если операционная система выполняет предварительную покраску, это просто приводит в порядок.

Некоторое время я думал, что это может быть связано с флагами классов CS_HREDRAW и CSVREDRAW.Однако реальность такова, что я не хочу, чтобы ОС попросила меня стереть окно - я просто хочу перерисовать себя без ОС, предварительно изменив содержимое моего окна (т.е. я хочу, чтобы дисплей был таким, какой он былдо того, как пользователь начал изменять размер - без каких-либо побитовых бликов от ОС).И я не хочу, чтобы ОС сообщала каждому элементу управления о том, что он также должен быть перерисован (если только это не был тот, который был фактически скрыт или обнаружен изменением размера.

Что я действительно хочу:

  1. Чтобы переместить и изменить размеры дочерних элементов управления до что-либо будет обновлено на экране.
  2. Полностью отобразить все перемещенные или измененные размеры дочерних элементов управления, чтобы ониотображаются без артефактов в их новом размере и расположении.
  3. Нарисуйте пробелы между дочерними элементами управления, не затрагивая сами дочерние элементы управления.

ПРИМЕЧАНИЕ. Шаги 2 и 3 можно изменить на противоположные.

Вышеупомянутые три вещи, кажется, происходят правильно, когда я использую DeferSetWindowPos () в сочетании с ресурсом диалога, помеченным как WS_CLIPCHILDREN.

Я бы получил дополнительное небольшое преимущество, если бы я мог сделать выше, чтобыDC памяти, а затем только один бит в конце обработчика WM_SIZE.

Я играл с этим некоторое время, и я не могу сбежатьДве вещи:

  1. Я все еще не могу запретить Windows выполнять «предсказательный битовый разряд». Ответ: См. Ниже решение, которое переопределяет WM_NCCALCSIZE, чтобы отключить это поведение.

  2. Я не вижу, как можно построить диалог, в котором его дочерние элементы управления растягиваются до двойногобуфер. Ответ: См. Ответ Джона (помеченный как ответ) ниже о том, как попросить ОС Windows дважды буферизовать ваш диалог (примечание: это запрещает любые промежуточные операции рисования GetDC (), в соответствии с документами).


Мое окончательное решение (Спасибо всем, кто помог, особенно Джон К.):

После долгих потов и слез я обнаружил, что следующая техникаработает без нареканий, как в Aero, так и в XP или с отключенным Aero.Flicking не существует (1).

  1. Подключите процедуру диалога.
  2. Переопределите WM_NCCALCSIZE, чтобы заставить Windows проверять всю клиентскую область, а не что-либо битовое.
  3. Переопределите WM_SIZE, чтобы выполнить все ваши шаги и изменить размеры, используя BeginDeferWindowPos / DeferWindowPos / EndDeferWindowPos для всех видимых окон.
  4. Убедитесь, что диалоговое окно имеет стиль WS_CLIPCHILDREN.
  5. НЕ использовать 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://stackoverflow.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 и т. 1079 *

Если будет достаточно интереса, меня могут убедить отредактировать мои выводы с помощью реального примера реализации для CodeProject.com или где-то подобного. Напишите мне.

(1) Обратите внимание, что невозможно избежать одной ничьи поверх того, что было раньше. Для каждой части диалогового окна, которая не изменилась, пользователь ничего не видит (никакого мерцания). Но там, где все изменилось, пользователю видны изменения - этого избежать невозможно, и это 100% решение.

Ответы [ 8 ]

14 голосов
/ 30 января 2010

Вы не можете предотвратить рисование во время изменения размера, но вы можете (с осторожностью) предотвратить перекрашивание , отсюда и мерцание.во-первых, bitblt.

Есть два способа остановить битовый бит.

Если у вас есть класс окна верхнего уровня, просто зарегистрируйте его в стилях 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.

4 голосов
/ 30 января 2010

Если я правильно понял вопрос, это точно вопрос Рэймонд обратился сегодня .

1 голос
/ 26 октября 2018

Вот обновление 2018 года, так как я только что пробежал ту же самую перчатку, что и вы.

«Окончательное решение» в вашем вопросе и связанные с ним ответы, в которых упоминаются приемы с WM_NCCALCSIZE и CS_HREDRAW|CS_VREDRAW, хороши для предотвращения выполнения Windows 100 / 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, по крайней мере, в соответствии с очень скудным dox на страницах 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. Наслаждайтесь:

Как сгладить уродливый джиттер / мерцание / прыжок при изменении размеров окон, особенно перетаскивая левую / верхнюю границу (Win 7-10; bg, bitblt и DWM)?

1 голос
/ 03 февраля 2010

Для некоторых элементов управления вы можете использовать сообщение WM_PRINT, чтобы преобразовать элемент в DC. Но это на самом деле не решает вашу главную проблему: вы хотите, чтобы Windows НЕ рисовала что-либо во время изменения размера, а позволяла вам делать все это.

И ответ таков: вы просто не можете делать то, что хотите, пока у вас есть дочерние окна.

То, как я в конечном итоге решил эту проблему в своем собственном коде, это переключение на использование Безоконных элементов управления . Поскольку у них нет собственного окна, они всегда рисуют в одно и то же время (и в том же DC), что и их родительское окно. Это позволяет мне использовать простую двойную буферизацию, чтобы полностью удалить мерцание. Я даже тривиально подавляю рисование детей, когда мне нужно просто , а не вызывать их процедуру рисования внутри процедуры рисования родителя.

Это единственный известный мне способ полностью избавиться от мерцания и разрыва при операциях изменения размера.

0 голосов
/ 01 февраля 2010

Что, кажется, работает:

  1. Использовать WS_CLIPCHILDREN в родительском диалоге (можно установить в WM_INITDIALOG)
  2. Во время WM_SIZE перебирайте дочерние элементы управления, перемещая и изменяя их размер с помощью DeferSetWindowPos ().

Это очень близко к идеалу, в моем тестировании под Windows 7 с Aero.

0 голосов
/ 30 января 2010

Существует только один способ эффективной диагностики проблем с перекрашиванием - удаленная отладка.

Получите второй ПК. Установите MSVSMON на нем. Добавьте шаг после сборки или служебный проект, который копирует ваши продукты сборки на удаленный ПК.

Теперь вы должны иметь возможность размещать точки останова в обработчиках WM_PAINT, обработчиках WM_SIZE и т. Д. И фактически отслеживать ваш код диалога, когда он выполняет размер и перерисовку. Если вы загрузите символы с серверов символов MS, вы сможете увидеть полные стеки вызовов.

Некоторые удачно расположенные точки останова - в ваших обработчиках WM_PAINT, WM_ERAGEBKGND, и вы должны иметь хорошее представление о том, почему ваше окно синхронно перерисовывается на ранних этапах цикла WM_SIZE.

В системе есть МНОЖЕСТВО окон, которые состоят из родительского окна с многоуровневыми дочерними элементами управления - окна проводника массово усложняются представлениями списков, панелями предварительного просмотра древовидных структур и т. Д. У проводника нет проблем с мерцанием при изменении размера, поэтому он явно можно изменить размер родительских окон без мерцания: - вам нужно перехватить перерисовки, выяснить, что их вызвало, и, в общем, убедиться, что причина устранена.

0 голосов
/ 30 января 2010

Существуют различные подходы, но я обнаружил, что обычно можно использовать только двойную буферизацию: отрисовка за пределами экрана, затем перетаскивание всего буфера на экран.

Это бесплатно в Vista Aero и выше, так что ваша боль может быть недолгой.

Мне неизвестна общая реализация двойной буферизации для окон и системных элементов управления под XP, однако вот несколько вещей, которые нужно изучить:

CMemDC Кита Рулета для двойной буферизации всего, что вы рисуете с помощью GDI
WS_EX_COMPOSITED Стиль окна (см. Раздел «Примечания» и что-то здесь в stackoverflow )

0 голосов
/ 30 января 2010

Если вы можете найти место для его подключения, CWnd::LockWindowUpdates() предотвратит появление любого рисунка до тех пор, пока вы не разблокируете обновления.

Но имейте в виду, что это хак, причем довольно уродливый. Ваше окно будет выглядеть ужасно при изменении размеров. Если проблема, с которой вы сталкиваетесь, заключается в мерцании во время изменения размера, тогда лучше всего диагностировать мерцание, а не скрывать мерцание, блокируя краски.

Одна вещь, которую нужно искать, это команды перерисовки, которые вызываются слишком часто во время изменения размера. Если элементы управления вашего окна вызывают RedrawWindow() с указанным флагом RDW_UPDATENOW, он будет перекрашиваться тогда и там. Но вы можете убрать этот флаг и указать RDW_INVALIDATE вместо этого, что говорит элементу управления, чтобы сделать окно недействительным без перерисовки. Он будет перекрашиваться во время простоя, сохраняя дисплей свежим без разбрызгивания.

...