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

ПРОБЛЕМА. Когда я беру границу изменения размера моего приложения Windows, особенно верхнюю или левую границы, и изменяю размер окна, содержимое окна изменяет размер "вживую" при перетаскивании, но оно изменяется отвратительным образом, похожим на вопиющая ошибка даже самого начинающего пользователя: содержимое на противоположном краю окна от края, который я дико перетаскиваю джиттер / мерцание / скачок назад и вперед. В зависимости от ситуации явление может выглядеть следующим образом:

  • содержимое, которое, кажется, сходит с края окна и возвращается назад, когда мы замедляемся или перестаем перетаскивать
  • содержимое, которое, кажется, тянет в окно, периодически смещается границей различных цветов, часто черного или белого
  • серьезно уродливое "двойное изображение" с двумя перекрывающимися копиями контента, смещенными на расстояние, пропорциональное тому, насколько / насколько быстро мы перетаскиваем

Ужасное явление прекращается, как только я прекращаю перетаскивать, но во время перетаскивания это заставляет приложение выглядеть любительским и непрофессиональным.

Не стоит преуменьшать, что эта проблема Windows свела с ума тысячи разработчиков приложений.

Вот два примера картины этого явления, любезно подготовленные к смежному вопросу Roman Starkov:

пример 1: джиттер

пример 2: граница

Другой пример, показывающий злое явление "двойного изображения" (обратите внимание на быстрые вспышки) от Kenny Liu:

пример 2: двойное изображение

Еще один пример видео о явлении с помощью диспетчера задач здесь.

ЦЕЛЬ НАСТОЯЩЕГО ВОПРОСА. Любой разработчик, столкнувшийся с этой проблемой, быстро обнаруживает, что существует по меньшей мере 30 вопросов Stackru, некоторые из которых были недавно, а некоторые возникли в 2008 году, полные многообещающих ответов, которые редко работают. Реальность такова, что эта проблема имеет много причин, и существующие вопросы / ответы Stackru никогда не проясняют более широкий контекст. Этот вопрос стремится ответить:

  • Каковы наиболее вероятные причины этого вида уродливого дрожания / мерцания / прыжков?
  • как мне сказать, что, потому что я вижу?
  • это причина специфическая для конкретных графических драйверов или общая для Windows?
  • как я могу исправить каждую причину? может ли приложение это исправить?

СФЕРА ЭТОГО ВОПРОСА: Для объема этого вопроса Stackru, явление происходит с:

  • как собственные Win32, так и управляемые приложения.NET/WPF/Windows Forms
  • и обычные окна Win32 и окна диалога Win32
  • Версии Windows, включая XP, Vista, 7, 8 и 10 (но см. Ниже темную правду о множественных причинах)

НЕ ФЛАГИРУЙТЕ ШИРОКИЙ ИЛИ ДУПАЙ: Пожалуйста, прочитайте полный ответ ниже, прежде чем отмечать как дупли или отмечать широко. Мы уже прошли этот цикл, и пользователи SO впоследствии вновь открыли вопрос, поняв следующее: этот вопрос приносит пользу сообществу, поскольку он является первым Q&A, объясняющим все различные причины дрожания изменения размера окна, чтобы пользователи могли определить, какой из причины вызывает их проблему и решить ее. Приведенные ниже ответы являются длинными, просто потому, что сложно объяснить, почему джиттер возникает в целом, а не потому, что этот вопрос охватывает множество отдельных ошибок / проблем, которые будут лучше обслуживаться отдельными вопросами и ответами. Как вы увидите, все приведенные выше перестановки (нативная / управляемая, окно / диалоговое окно, XP-10) сводятся только к двум основным причинам, но выявление того, что у вас есть, является сложной задачей. Вот о чем этот вопрос. Разделение вопроса полностью сведет на нет эту выгоду. Мы дополнительно ограничим сферу вопроса в следующем разделе...

НЕ В СФЕРЕ ЭТОГО ВОПРОСА:

  • Если ваше приложение имеет одно или несколько дочерних окон (дочерних HWND), информация в этом вопросе будет полезна для вас (так как вызывает рывки BitBlts мы опишем, применяются ли они к вашим дочерним окнам вместе с родительским окном), но во время изменения размера окна у вас возникает дополнительная проблема, которая выходит за рамки этого вопроса: вам нужно заставить все ваши дочерние окна двигаться атомарно и синхронно с родительское окно. Для этой задачи вы, вероятно, захотите BeginDeferWindowPos/DeferWindowPos/EndDeferWindowPos и вы можете узнать о них здесь и здесь.

  • Этот вопрос предполагает, что если ваше приложение обращается к окну с использованием GDI, DirectX или OpenGL, то вы уже реализовали WM_ERASEBKGND обработчик в вашем wndproc это просто возвращает 1. WM_ERASEBKGND тайный остаток Windows от Windows 3.1, который предшествует WM_PAINT чтобы дать вашему приложению возможность "стереть фон" вашего окна, прежде чем вы начнете рисовать окно... ага. Если вы позволите WM_ERASEBKGND сообщение входит в DefWindowProc()Это приведет к тому, что все ваше окно будет окрашено в сплошной цвет, обычно белый, на каждой перерисовке, включая перерисовки, которые происходят во время изменения размера живого окна. В результате получается мерзкое мерцание в полноэкранном режиме, но не тот тип джиттера / мерцания / прыжков, о котором мы говорим в этом вопросе. Перехватив WM_ERASEBKGND решает эту проблему немедленно.

  • Этот вопрос в первую очередь касается изменения размера в реальном времени путем перетаскивания границ окна с помощью мыши. Тем не менее, многое из того, что написано здесь, также относится к уродливым артефактам, которые вы можете увидеть, когда приложение вручную изменяет размер окна, используя SetWindowPos(), Они менее заметны, потому что они мерцают на экране только на одно мгновение, а не в течение длительного периода перетаскивания.

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

  • Если ваше приложение вообще не может изменить размер во время изменения размера приложения (например, оно "зависает" во время изменения размера, особенно если это OpenGL, использующий GLFW или другую библиотеку), посмотрите эти другие вопросы, которые объясняют ужасный вложенный / модальный цикл событий Microsoft внутри. WM_SYSCOMMAND во время перетаскивания: вот особенно этот хороший ответ, здесь, здесь, здесь и здесь.

НАЧАЛЬНЫЕ ОТВЕТЫ

Для начала мы предоставили два начальных ответа, которые вы должны прочитать в следующем порядке:

а также список исходного материала, который может помочь другим понять:

Мы надеемся, что люди дадут больше ответов с творческими способами избежать проблем, описанных в 2 a и особенно 2b.

7 ответов

Решение

Оглавление

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

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

Пожалуйста, не стесняйтесь давать больше ответов с творческими способами избежать проблем, описанных в 2a и особенно 2b!

ЧАСТЬ 2: выявление и устранение проблем с изменением размера Windows

Примечание: сначала вы хотите прочитать ЧАСТЬ 1, чтобы этот ответ имел смысл.

Этот ответ не решит все ваши проблемы с изменением размера.

Он организует все еще используемые идеи из других постов и добавляет несколько новых идей.

Ни одно из этих действий вообще не задокументировано на MSDN Microsoft, и то, что следует ниже, является результатом моего собственного эксперимента и просмотра других сообщений Stackru.

2а. Изменить размер проблемы с 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,

Помните, мы предполагаем, что вы предоставили WM_ERASEBKGND обработчик, который возвращает 1 (см."НЕ В СФЕРЕ ЭТОГО ВОПРОСА" в вопросе выше), так что сообщение ничего не делает, и мы можем его игнорировать.

Во время обработки этих сообщений (вскоре после WM_WINDOWPOSCHANGING возвращается), Windows делает внутренний вызов SetWindowPos() на самом деле изменить размер окна. Тот SetWindowPos() Сначала вызов изменяет размер области, не являющейся клиентом (например, строки заголовка и границы окна), а затем переключает свое внимание на область клиента (основную часть окна, за которую вы отвечаете).

В течение каждой последовательности сообщений от одного перетаскивания Microsoft дает вам определенное время для самостоятельного обновления клиентской области.

Часы этого срока, видимо, начинают тикать после WM_NCCALCSIZE возвращается. В случае окон OpenGL крайний срок, по-видимому, удовлетворен, когда вы звоните SwapBuffers() представить новый буфер (не когда ваш WM_PAINT вводится или возвращается). Я не использую GDI или DirectX, поэтому я не знаю, что эквивалентно SwapBuffers() есть, но вы, вероятно, можете сделать правильное предположение, и вы можете проверить, вставив Sleep(1000) в различных точках вашего кода, чтобы увидеть, когда приведенное ниже поведение срабатывает.

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

Если вы действительно обновите свою клиентскую область к установленному сроку, то Microsoft оставит вашу клиентскую область красиво безупречной. Ваш пользователь будет видеть только те пиксели, которые вы рисуете, и у вас будет максимально плавное изменение размера.

Если вы не обновите свою клиентскую область к крайнему сроку, тогда Microsoft вмешается и "поможет" вам, сначала показав некоторые другие пиксели вашему пользователю на основе комбинации метода "Заполнить некоторым цветом фона" (раздел 1c3 из ЧАСТЬ 1) и техника "Отрежьте несколько пикселей" (Раздел 1c4 ЧАСТИ 1). Что именно пиксели Microsoft показывает вашему пользователю, хорошо, сложно:

  • Если ваше окно имеет WNDCLASS.style это включает в себя CS_HREDRAW|CS_VREDRAW биты (вы передаете структуру WNDCLASS RegisterClassEx):

    • Нечто удивительно разумное происходит. Вы получаете логическое поведение, показанное на рисунках 1c3-1, 1c3-2, 1c4-1 и 1c4-2 ЧАСТИ 1. При увеличении клиентской области Windows будет заполнять новые открытые пиксели "фоновым цветом" (см. Ниже) на той же стороне перетаскиваемого окна. При необходимости (левая и верхняя границы), Microsoft делает BitBlt чтобы сделать это. При уменьшении клиентской области Microsoft отсекает пиксели на той же стороне окна, которое вы перетаскиваете. Это означает, что вы избегаете поистине отвратительного артефакта, из-за которого объекты в вашей клиентской области кажутся движущимися в одном направлении, а затем движутся назад в другом направлении.

    • Это может быть достаточно, чтобы обеспечить приемлемое поведение при изменении размера, если только вы действительно не хотите его подтолкнуть и посмотреть, сможете ли вы полностью запретить Windows приставать к вашей клиентской области, прежде чем у вас появится возможность рисовать (см. Ниже).

    • Не реализуйте свой собственный WM_NCCALCSIZE Обработчик в этом случае, чтобы избежать глючного поведения Windows, описанного ниже.

  • Если ваше окно имеет WNDCLASS.style это не включает CS_HREDRAW|CS_VREDRAW биты (включая диалоги, где Windows не позволяет вам установить WNDCLASS.style):

    • Windows пытается "помочь", выполняя BitBlt это делает копию определенного прямоугольника пикселей из вашей старой клиентской области и записывает этот прямоугольник в определенное место в вашей новой клиентской области. это BitBlt 1:1 (он не масштабирует и не увеличивает ваши пиксели).

    • Затем Windows заполняет другие части новой клиентской области (части, которые Windows не перезаписывала во время BitBlt операция) с "фоновым цветом."

    • BitBlt Операция часто является основной причиной, почему изменение размера выглядит так плохо. Это связано с тем, что Windows неправильно оценивает, как ваше приложение будет перерисовывать клиентскую область после изменения размера. Windows размещает ваш контент не в том месте. Чистый результат заключается в том, что когда пользователь впервые видит BitBlt пикселей, а затем видит реальные пиксели, нарисованные вашим кодом, ваш контент сначала перемещается в одном направлении, а затем возвращается назад в другом направлении. Как мы объяснили в ЧАСТИ 1, это создает самый отвратительный тип артефакта изменения размера.

    • Таким образом, большинство решений для устранения проблем изменения размера включают отключение BitBlt,

    • Если вы реализуете WM_NCCALCSIZE обработчик и этот обработчик возвращает WVR_VALIDRECTS когда wParam 1, вы можете контролировать, какие пиксели Windows копирует (BitBlts) из старой клиентской области и места, где Windows размещает эти пиксели в новой клиентской области. WM_NCCALCSIZE едва документирован, но посмотрите на подсказки о WVR_VALIDRECTS а также NCCALCSIZE_PARAMS.rgrc[1] and [2] на страницах MSDN для WM_NCCALCSIZE а также NCCALCSIZE_PARAMS, Вы даже можете предоставить NCCALCSIZE_PARAMS.rgrc[1] and [2] возвращать значения, которые полностью мешают Windows BitBlting любой из пикселей старой клиентской области к новой клиентской области, или заставить Windows BitBlt один пиксель от одного и того же местоположения, что фактически одно и то же, поскольку никакие пиксели на экране не будут изменены. Просто установите оба NCCALCSIZE_PARAMS.rgrc[1] and [2] к тому же 1-пиксельному прямоугольнику. В сочетании с устранением "цвета фона" (см. Ниже), это дает вам возможность предотвратить растушевку пикселей Windows, прежде чем вы успеете их нарисовать.

    • Если вы реализуете WM_NCCALCSIZE обработчик, и он возвращает что-либо кроме WVR_VALIDRECTS когда wParam 1, то вы получите поведение, которое (по крайней мере, в Windows 10) совсем не похоже на то, что говорит MSDN. Кажется, Windows игнорирует все флаги выравнивания по левому / правому / верхнему / нижнему краям, которые вы возвращаете. Я советую вам не делать этого. В частности, популярная статья Stackru Как заставить Windows НЕ перерисовывать что-либо в моем диалоге, когда пользователь изменяет размер моего диалога? возвращается WVR_ALIGNLEFT|WVR_ALIGNTOP и это, кажется, полностью сломано, по крайней мере, в моей тестовой системе Windows 10. Код в этой статье может работать, если он изменен на возвращение WVR_VALIDRECTS вместо.

    • Если у вас нет собственного кастома WM_NCCALCSIZE обработчик, вы получите довольно бесполезное поведение, которого, вероятно, лучше избегать:

      • Если вы уменьшаете клиентскую область, ничего не происходит (ваше приложение не получает WM_PAINT совсем)! Если вы используете верхнюю или левую границу, содержимое вашей клиентской области будет перемещаться вместе с верхним левым краем клиентской области. Чтобы получить любое живое изменение размеров при уменьшении окна, вы должны вручную нарисовать из wndproc сообщение как WM_SIZE, или позвоните по телефону InvalidateWindow() вызвать позже WM_PAINT,

      • Если вы увеличите клиентскую зону

        • Если вы перетаскиваете нижнюю или правую границу окна, Microsoft заполняет новые пиксели "фоновым цветом" (см. Ниже).

        • Если вы перетащите верхнюю или левую границу окна, Microsoft скопирует существующие пиксели в верхний левый угол расширенного окна и оставит старую ненужную копию старых пикселей во вновь открытом пространстве.

Итак, как вы можете видеть из этой грязной истории, есть две полезные комбинации:

  • 2a1. WNDCLASS.style с CS_HREDRAW|CS_VREDRAW дает вам поведение на рисунках 1c3-1, 1c3-2, 1c4-1 и 1c4-2 ЧАСТИ 1, которое не является идеальным, но, по крайней мере, содержимое вашей клиентской области не будет двигаться в одном направлении, а затем будет двигаться назад в другом направлении

  • 2a2. WNDCLASS.style без CS_HREDRAW|CS_VREDRAW плюс WM_NCCALCSIZE возврат обработчика WVR_VALIDRECTS (когда wParam это 1) что BitBlts ничего, плюс отключение "цвета фона" (см. ниже) может полностью отключить растление Windows вашей клиентской области.

Существует, по-видимому, еще один способ достижения эффекта комбинации 2a2. Вместо реализации своего WM_NCCALCSIZE Вы можете перехватить WM_WINDOWPOSCHANGING (сначала передавая его на DefWindowProc) и установить WINDOWPOS.flags |= SWP_NOCOPYBITS, который отключает BitBlt внутри внутреннего звонка SetWindowPos() что Windows делает во время изменения размера окна. Я сам не пробовал этот трюк, но многие пользователи SO сообщили, что он работает.

В нескольких пунктах выше мы упоминали "цвет фона". Этот цвет определяется WNDCLASS.hbrBackground поле, которое вы передали RegisterClassEx, Это поле содержит HBRUSH объект. Большинство людей устанавливает его, используя следующий стандартный код:

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

COLOR_WINDOW+1 заклинание дает вам белый цвет фона. См. MSDN dox для WNDCLASS для объяснения +1 и обратите внимание, что есть много неверной информации о +1 на Stackru и форумах MS.

Вы можете выбрать свой собственный цвет, как это:

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

Вы также можете отключить фоновое заполнение, используя:

wndclass.hbrBackground = NULL;

который является другим ключевым ингредиентом комбинации 2a2 выше. Но имейте в виду, что вновь открытые пиксели будут принимать какой-то по существу случайный цвет или рисунок (какой бы мусор ни находился в вашем графическом буфере кадров), пока ваше приложение не догонит и не отрисовает новые пиксели клиентской области, поэтому на самом деле может быть лучше использовать комбинацию 2a1 и выберите цвет фона, который подходит для вашего приложения.

2b. Проблемы с изменением размера при заполнении композиции DWM

В какой-то момент во время разработки Aero Microsoft добавила еще одну проблему дрожания живого изменения размера поверх проблемы всех версий Windows, описанной выше.

Читая ранее сообщения Stackru, на самом деле трудно сказать, когда возникла эта проблема, но мы можем сказать, что:

  • эта проблема определенно возникает в Windows 10
  • эта проблема почти наверняка возникает в Windows 8
  • эта проблема могла также возникать в Windows Vista с включенным Aero (во многих сообщениях с проблемами изменения размера под Vista не говорится, было ли у них включено Aero или нет).
  • эта проблема, вероятно, не возникала в Windows 7, даже с включенным Aero.

Проблема связана с серьезным изменением архитектуры, представленной Microsoft в Windows Vista, под названием DWM Desktop Composition. Приложения больше не рисуют напрямую в графический фрейм-буфер. Вместо этого все приложения фактически рисуют в закадровом кадровом буфере, который затем комбинируется с выходом других приложений новым, злым процессом Windows Window Manager (DWM).

Таким образом, поскольку для отображения ваших пикселей используется еще один процесс, существует еще одна возможность испортить ваши пиксели.

И Microsoft никогда не упустит такую ​​возможность.

Вот что, по-видимому, происходит с составом DWM:

  • Пользователь щелкает мышью по границе окна и начинает перетаскивать мышь

  • Каждый раз, когда пользователь перетаскивает мышь, это вызывает последовательность wndproc события в вашем приложении, которые мы описали в разделе 2а выше.

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

  • Как и в разделе 2а выше, таймер начинает тикать после WM_NCCALCSIZE возвращается и удовлетворен, когда ваше приложение рисует и звонит SwapBuffers(),

  • Если вы действительно обновите свою клиентскую область к крайнему сроку, то DWM оставит вашу клиентскую область красиво безупречной. Существует определенная вероятность того, что ваша клиентская область все еще может быть осквернена проблемой в разделе 2a, поэтому обязательно прочитайте также раздел 2a.

  • Если вы не обновите свою клиентскую область к крайнему сроку, тогда Microsoft сделает что-то действительно отвратительное и невероятно плохое (разве Microsoft не усвоила их урок?):

    • Предположим, что это ваша клиентская область до изменения размера, где A, B, C и D представляют цвета пикселей в середине верхней, левой, правой и нижней границ вашей клиентской области:
    -------------- AAA -----------------
    | |
    До нашей эры
    До нашей эры
    До нашей эры
    |                                |
    --------------DDD-----------------
    
    • Предположим, вы используете мышь для увеличения клиентской области в обоих измерениях. Genius Windows DWM (или, возможно, Nvidia: подробнее об этом позже) всегда будет копировать пиксели вашей клиентской области в верхний левый угол новой клиентской области (независимо от того, какую границу окна вы перетаскиваете), а затем делать самые абсурдные вещи. мыслимый для остальной части клиентской зоны. Windows возьмет любые пиксельные значения, которые раньше были вдоль нижнего края вашей клиентской области, растянет их до новой ширины клиентской области (ужасная идея, которую мы исследовали в Разделе 1c2 ЧАСТИ 1), и скопирую эти пиксели, чтобы заполнить все новые открытое пространство внизу (посмотрите, что происходит с D). Затем Windows возьмет любые пиксельные значения, которые были вдоль правого края вашей клиентской области, растянет их до новой высоты клиентской области и скопирует их, чтобы заполнить новые открытое пространство в правом верхнем углу:
    -------------- AAA ----------------------------------- ------------
    | | |
    До н.э.
    До н.э.
    B                                CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    |                                |CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    --------------DDD-----------------CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    |                             DDDDDDDDD                        |
    ------------------------------DDDDDDDDD-------------------------
    
    • Я даже не представляю, что они курят. Такое поведение дает наихудший возможный результат во многих случаях. Во-первых, при перетаскивании границ левого и верхнего окон почти гарантированно генерируется ужасающее движение вперед-назад, которое мы показали на рис. 1c3-3 и рис. 1c4-3 ЧАСТИ 1, поскольку скопированный прямоугольник всегда находится в верхнем левом углу. независимо от того, какую границу окна вы перетаскиваете. Во-вторых, еще более загадочная вещь, которая происходит с реплицируемыми краевыми пикселями, будет приводить к появлению некрасивых полос, если у вас есть какие-либо пиксели, установленные там, кроме цвета фона. Обратите внимание, что созданные полосы C и D даже не совпадают с исходными C и D из скопированных старых пикселей. Я могу понять, почему они реплицируют края, надеясь найти фоновые пиксели там, чтобы "автоматизировать" процесс обнаружения цвета фона, но кажется, что вероятность этого на самом деле сильно перевешивается фактором взлома и вероятностью отказа. Было бы лучше, если бы DWM использовал выбранный приложением "цвет фона" (в WNDCLASS.hbrBackground), но я подозреваю, что DWM может не иметь доступа к этой информации, так как DWM находится в другом процессе, отсюда и взлом. Вздох.

Но мы даже не дошли до худшей части:

  • Каков на самом деле крайний срок, который DWM дает вам для того, чтобы нарисовать свою собственную клиентскую область, прежде чем DWM испортит ее с помощью этого неуклюжего предположения? По-видимому (из моих экспериментов) крайний срок составляет порядка 10-15 миллисекунд! Учитывая, что 15 миллисекунд близки к 1/60, я бы предположил, что крайний срок - это конец текущего кадра. И подавляющее большинство приложений не могут уложиться в этот срок в большинстве случаев.

Вот почему, если вы запустите Windows Explorer в Windows 10 и перетащите левую границу, вы, скорее всего, увидите полосу прокрутки на правом джиттере / мерцании / скачкообразном скачке, как будто Windows была написана четвероклассником.

Я не могу поверить, что Microsoft выпустила подобный код и считает, что он "готов". Также возможно, что ответственный код находится в графическом драйвере (например, Nvidia, Intel, ...), но некоторые сообщения Stackru заставили меня поверить, что это поведение между устройствами.

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

Я действительно надеюсь, что какой-нибудь пользователь Stackru предложит какой-нибудь волшебный параметр DWM или флаг в Windows 10, который мы можем сделать, чтобы продлить срок или полностью отключить ужасное поведение.

Но в то же время я придумал один хак, который несколько снижает частоту отвратительных взад-вперед артефактов при изменении размера окна.

Хак, вдохновленный комментарием на /questions/7286040/ne-mogu-izbavitsya-ot-drozhaniya-pri-peretaskivanii-levoj-granitsyi-okna/7286057#7286057, состоит в том, чтобы приложить максимум усилий для синхронизации процесса приложения с вертикальным возвратом, который управляет активностью DWM. На самом деле сделать эту работу в Windows не тривиально. Код для этого хака должен быть самым последним в вашей 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);

Вы можете убедить себя в том, что этот хак работает, раскомментировав строку, демонстрирующую поведение в "наихудшем случае", пытаясь запланировать рисование прямо в середине кадра, а не при вертикальной синхронизации, и заметив, сколько у вас еще артефактов. Вы также можете попытаться изменить смещение в этой строке медленно, и вы увидите, что артефакты внезапно исчезают (но не полностью) примерно через 90% периода и возвращаются снова через 5-10% периода.

Поскольку Windows не является операционной системой реального времени, ваше приложение может быть выгружено в любом месте этого кода, что приводит к неточности в сопряжении now1 а также dti.qpcVBlank, Вытеснение в этом небольшом разделе кода встречается редко, но возможно. Если хотите, можете сравнить now0 а также now1 и снова зациклиться, если граница недостаточно тугая. Для упреждения также возможно нарушить сроки Sleep() или код до или после Sleep(), Вы ничего не можете с этим поделать, но оказывается, что ошибки синхронизации в этой части кода завалены неуверенным поведением DWM; Вы все еще будете получать некоторые артефакты изменения размера окна, даже если ваше время идеально. Это просто эвристика.

Есть второй взлом, и он невероятно креативный: как объяснено в посте Stackru Не удается избавиться от дрожания при перетаскивании левой границы окна, вы можете создать два основных окна в своем приложении и каждый раз Windows сделает SetWindowPos Вы поймете это и вместо этого прячете одно окно и показываете другое! Я еще не пробовал это, но OP сообщает, что он обходит безумную пиксельную копию пикселя DWM, описанную выше.

Существует третий способ взлома, который может работать в зависимости от вашего приложения (особенно в сочетании с указанным выше взломом синхронизации). Во время живого изменения размера (которое вы можете обнаружить, перехватывая WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE), вы могли бы изменить свой код рисования, чтобы изначально нарисовать что-то намного более простое, что с гораздо большей вероятностью завершится в срок, установленный задачами 2a и 2b, и вызвать SwapBuffers() чтобы получить свой приз: этого будет достаточно, чтобы Windows не выполнила плохой блиц / заливку, описанную в разделах 2a и 2b. Затем, сразу после частичного рисования, сделайте еще один розыгрыш, который полностью обновляет содержимое окна, и вызовите SwapBuffers() снова. Это может все еще выглядеть несколько странно, поскольку пользователь увидит обновление вашего окна в двух частях, но, скорее всего, оно будет выглядеть намного лучше, чем отвратительный артефакт движения назад и вперед от Windows.

Еще один интересный момент: некоторые приложения в Windows 10, включая консоль (запуск cmd.exe), не содержат артефактов DWM Composition даже при перетаскивании левой границы. Так что есть какой-то способ обойти проблему. Давайте найдем это!

2с. Как диагностировать вашу проблему

Когда вы пытаетесь решить вашу конкретную проблему изменения размера, вы можете задаться вопросом, какой из перекрывающихся эффектов из Раздела 2a и Раздела 2b вы видите.

Один из способов разделить их - немного отладить в Windows 7 (с отключенным Aero, просто для безопасности).

Другой способ быстро определить, видите ли вы проблему в Разделе 2b, - изменить приложение, чтобы оно отображало тестовый шаблон, описанный в Разделе 2b, как в этом примере (обратите внимание на цветные линии толщиной в 1 пиксель на каждом из четырех краев):

тестовый образец

Затем возьмите любую границу окна и начните быстро изменять ее размер. Если вы видите прерывистые гигантские цветные полосы (синие или зеленые полосы в случае этого тестового шаблона, поскольку на нижнем краю есть синий, а на правом - зеленый), то вы знаете, что видите проблему в разделе 2b.

Вы можете проверить, видите ли вы проблему в Разделе 2а, установив WNDCLASS.hbrBackground с отличным цветом фона, как красный. При изменении размера окна, новые экспонированные части будут отображаться с этим цветом. Но прочитайте Раздел 2a, чтобы убедиться, что ваши обработчики сообщений не вызывают Windows BitBlt вся клиентская область, что заставит Windows не рисовать фоновый цвет.

Помните, что проблемы в Разделах 2a и 2b обнаруживаются только в том случае, если ваше приложение не может рисовать к определенному крайнему сроку, а каждая проблема имеет свой крайний срок.

Таким образом, без изменений ваше приложение может показывать только проблему раздела 2b, но если вы измените свое приложение, чтобы рисовать медленнее (вставьте Sleep() в WM_PAINT до SwapBuffers() например), вы можете пропустить крайний срок для Раздела 2a и Раздела 2b и начать видеть обе проблемы одновременно.

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

ЧАСТЬ 1: Что делает изменение размера хорошим или плохим?

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

Это то, что мы будем делать в этом разделе.

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

Ниже мы будем ссылаться на окно

  • "область, не относящаяся к клиенту": часть окна, которой управляет Windows, включая строку заголовка вверху и границы окна по всем краям, и

  • "клиентская область": основная часть окна, за которую вы отвечаете

Предположим, у вас есть приложение с:

  • кнопка или метка L, которая должна оставаться вровень-влево
  • кнопка или метка R, которая должна оставаться вровень-вправо

независимо от того, как окно изменяется.

Ваше приложение может рисовать сам L/R (например, используя GDI/OpenGL/DirectX внутри одного окна) или L/R может быть неким элементом управления Microsoft (который будет иметь свой собственный HWND, отдельный от вашего главного окна HWND); не имеет значения

Вот упрощенное представление клиентской области окна вашего приложения. Как вы можете видеть, у нас есть LLL шириной в три столбца в дальнем левом углу клиентской области и RRR шириной в три столбца в крайнем правом углу клиентской области, с различным другим содержимым клиентской области, представленным как "-" в между (пожалуйста, игнорируйте серый фон, который Stackru настаивает на добавлении; L и R находятся на крайнем левом и правом краях вашей клиентской области):

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

Теперь представьте, что вы берете левую или правую границу этого окна и перетаскиваете его, чтобы сделать окно больше или меньше.

1a. Легкий случай: рисование вовремя

Представьте, что ваше приложение очень быстро рисует, так что оно всегда может реагировать на действие перетаскивания пользователя в течение 1 миллисекунды, а ОС позволяет вашему приложению рисовать это быстро, не пытаясь нарисовать что-либо еще на экране, чтобы "помочь" вам.

Когда вы перетаскиваете границу приложения, пользователь видит на экране следующее (каждая строка этих цифр представляет один момент времени):

Перетаскивание правой границы вправо (увеличение ширины):

(Рисунок 1a-1)
LLL-----------RRR     (изначально, когда вы щелкаете мышью)
LLL------------RRR    (когда вы перетаскиваете мышь)
LLL-------------RRR   (при перетаскивании мыши)
LLL--------------RRR  (при отпускании мыши) 

Перетаскивание правой границы влево (ширина сжатия):

(Рисунок 1a-2)
LLL-----------RRR
LLL----------RRR
LLL---------RRR
LLL--------RRR

Перетаскивание левой границы влево (увеличение ширины):

(Рисунок 1a-3)
   LLL-----------RRR
  LLL------------RRR
 LLL-------------RRR
LLL--------------RRR

Перетаскивание левой границы вправо (ширина сжатия):

(Рисунок 1a-4)
LLL-----------RRR
 LLL----------RRR
  LLL---------RRR
   LLL--------RRR

Все это выглядит хорошо и гладко:

  • При настройке правой границы, R, кажется, движется с постоянной скоростью в одном направлении, а L остается на месте, как и должно быть.
  • При настройке левой границы, L, кажется, движется с постоянной скоростью в одном направлении, а R остается неподвижным, как и должно.

Все идет нормально.

1б. Трудный случай: рисование отстает

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

  • что должно показать на экране в этот период, и
  • кто решает что показать?

Например, при перетаскивании правой границы вправо (увеличение ширины):

(Рисунок 1b-1)
LLL-----------RRR??????????????????    (что должно показать здесь?)???????????????????   (что должно показать здесь?)
LLL--------------RRR  (приложение догоняет)

В качестве другого примера, при перетаскивании левой границы влево (уменьшение ширины):

(Рисунок 1b-2)
LLL-----------RRR????????????????  (что должно показать здесь?)???????????????  (что должно показать здесь?)
   LLL--------RRR  (приложение догоняет)

Это ключевые вопросы, которые определяют, выглядит ли движение гладким или нет, и они являются ключевыми вопросами, вокруг которых вращается весь этот вопрос Stackru.

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

1c. Временные решения в ожидании приложения для рисования

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

1c1. Ничего не делать

Экран может оставаться таким же, как и до тех пор, пока приложение не догонит (ни пиксели клиента, ни даже граница окна в области, не являющейся клиентом, не изменятся):

Пример при перетаскивании правой границы вправо (увеличение ширины):

(Рисунок 1c1-1)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
LLL--------------RRR  (приложение догоняет)

Пример при перетаскивании левой границы влево (ширина сжатия):

(Рисунок 1c1-2)
LLL-----------RRR
LLL-----------RRR
LLL-----------RRR
   LLL--------RRR  (приложение догоняет)

Очевидный недостаток этого метода заключается в том, что в течение рассматриваемого периода приложение, по-видимому, зависло и, по-видимому, не реагирует на движения мыши, поскольку ни R, ни '-', ни L, ни граница окна не являются перемещение.

Microsoft часто считают, что Windows не отвечает на запросы (иногда это их вина, а иногда и ошибка разработчика приложения), поэтому с тех пор, как Microsoft представила live-resize (Windows XP?), Microsoft никогда не использовала метод "ничего не делать". само собой.

Метод "ничего не делать" раздражает пользователя и выглядит непрофессионально, но оказывается (очень неочевидно), что это не всегда худший выбор. Читать дальше...

1c2. Масштаб содержимого

Другая возможность заключается в том, что Windows всегда может заставить границу окна следовать вашим движениям мыши мгновенно (поскольку сама Windows обладает достаточной вычислительной мощностью, чтобы хотя бы своевременно рисовать не-клиентскую область), и пока она ждет вашего приложения, Windows может возьмите старые пиксели клиентской области и масштабируйте эти пиксели вверх или вниз так же, как при увеличении / увеличении изображения, чтобы они "помещались" в меньшем или большем пространстве.

Этот метод, как правило, хуже, чем любой другой метод, потому что он приведет к размытому изображению вашего исходного контента, которое, вероятно, будет непропорциональным. Так что никто никогда не должен делать это в любом случае. За исключением, как мы увидим в ЧАСТИ 2, иногда Microsoft делает.

1С3. При увеличении заполните фоновым цветом

Другой способ, который может сработать при увеличении окна, заключается в следующем: Windows всегда может заставить границу окна мгновенно следовать вашим движениям мыши, и Windows может заполнить новые пиксели увеличенной клиентской области некоторым временным цветом фона B:

Например, при перетаскивании правой границы вправо (увеличение ширины):

(Рисунок 1c3-1)
LLL-----------RRR
LLL-----------RRRB
LLL-----------RRRBB
LLL--------------RRR  (приложение догоняет)

Этот метод имеет то преимущество, что в течение рассматриваемого периода, по крайней мере, граница вашего окна перемещается, поэтому приложение чувствует себя отзывчивым.

Еще одна приятная особенность заключается в том, что во время перетаскивания L остается на месте, как и должно быть.

Немного странно, что новое пространство, которое вы создаете при перетаскивании, заполняется каким-то случайным цветом, и еще более странно, что R фактически не перемещается до позднего времени (обратите внимание, что R в последний момент дергается влево на 3 столбца), но по крайней мере R движется только в правильном направлении. Это частичное улучшение.

Огромный и важный вопрос: какого цвета должен быть новый цвет фона B? Если B окажется черным, а ваше приложение будет иметь в основном белый фон, или наоборот, это будет намного страшнее, чем если B соответствует цвету фона вашего существующего контента. Как мы увидим в ЧАСТИ 2, Windows развернула несколько различных стратегий для улучшения выбора B.

Теперь рассмотрим ту же идею, но вместо этого примените ее к случаю, когда мы перетаскиваем левую границу влево (увеличение ширины).

Логично было бы добавить новый цвет фона в левой части окна:

(Рисунок 1c3-2)
   LLL-----------RRR
  BLLL-----------RRR
 BBLLL-----------RRR
LLL--------------RRR  (приложение догоняет)

Это было бы логично, потому что R останется на месте, как и должно быть. У L будет та же странность, которую мы описали вместе с рисунком 1c3-1 выше (L будет стоять неподвижно, а затем в последний момент внезапно дергает 3 столбца влево), но по крайней мере L будет двигаться только в правильном направлении.

Однако - и это действительно станет шоком - в нескольких важных случаях, с которыми вам приходится иметь дело, Windows не делает логическую вещь.

Вместо этого Windows иногда заполняет фоновые пиксели B справа, даже если вы перетаскиваете левую границу окна:

(Рисунок 1c3-3)
   LLL-----------RRR
  LLL-----------RRRB
 LLL-----------RRRBB
LLL--------------RRR  (приложение догоняет)

Да, это безумие.

Посмотрите, как это выглядит для пользователя:

  • L, кажется, движется очень плавно с постоянной скоростью в одном направлении, так что это на самом деле хорошо, но

  • Просто посмотрите, что делает R:

      RRR
     RRR
    RRR  
      RRR (приложение догоняет)
    
    • R сначала перемещается влево на два столбца, чего не должно делать: R должен всегда оставаться вровень-вправо
    • Затем R снова возвращается вправо. Святое дерьмо!

Это выглядит ужасно, ужасно, ужасно, отвратительно... даже нет слов, чтобы описать, как плохо это выглядит.

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

Итак, здесь вы можете начать понимать, почему некоторые из этих уродливых проблем с изменением размера возникают только тогда, когда вы перетаскиваете левую (или верхнюю) границу, а не правую (или нижнюю) границу.

В действительности, оба случая (рисунок 1c3-2 против рисунка 1c3-3) делают что-то странное. На рисунке 1c3-2 мы временно добавляем фоновые пиксели B, которые там не принадлежат. Но это странное поведение гораздо менее заметно, чем движение вперед и назад на рисунке 1c3-3.

Это движение вперед-назад - это дрожание / мерцание / прыжки, о которых так много вопросов Stackru.

Поэтому любое решение проблемы плавного изменения размера должно:

  • по крайней мере, не позволяйте элементам в вашей клиентской области появляться, чтобы они прыгали в одном направлении, а затем назад в другом.

  • в идеале также избегайте необходимости добавлять фоновые пиксели B, если это возможно

1c4. При сжатии отрежьте несколько пикселей

Раздел 1c3 посвящен расширению окна. Если мы посмотрим на сжатие окна, то увидим, что существует точно такой же набор случаев.

При сжатии окна может сработать следующий метод: Windows всегда может заставить границу окна мгновенно следовать вашим движениям мыши, а Windows может просто отрубить (обрезать) некоторые пиксели вашей уменьшенной клиентской области.

Например, при перетаскивании правой границы влево (уменьшение ширины):

(Рисунок 1c4-1)
LLL-----------RRR
LLL-----------RR
LLL-----------R
LLL--------RRR     (приложение догоняет)

С помощью этой техники L остается на своем месте, как и должно быть, но странная вещь происходит справа: R, который должен оставаться на одном уровне, независимо от размера окна, по-видимому, получает правый край, постепенно срезанный правым краем клиентской области, пока R не исчезнет, ​​а затем внезапно R снова появится в правильном положении, когда приложение догонит. Это очень странно, но имейте в виду, что ни в одной точке R не движется вправо. Левый край R, кажется, остается неподвижным до последнего момента, когда все R отскакивают назад на 3 столбца влево. Таким образом, как мы видели на рисунке 1c3-1, R движется только в правильном направлении.

Теперь рассмотрим, что происходит, когда мы перетаскиваем левую границу вправо (уменьшение ширины).

Логично было бы стричь пиксели слева от клиентской области:

(Рисунок 1c4-2)
LLL-----------RRR
 LL-----------RRR
  L-----------RRR
   LLL--------RRR  (приложение догоняет)

Это будет иметь те же странные свойства, что и на рисунке 1c4-1, только с измененными ролями слева и справа. Казалось бы, что L постепенно сбривается от левого края L, но правый край L будет оставаться неподвижным до тех пор, пока в последний момент L не появится, чтобы прыгнуть вправо. Таким образом, L движется только в правильном направлении, хотя и резко.

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

Вместо этого Windows иногда обрезает пиксели справа, даже если вы перетаскиваете левую границу окна:

(Рисунок 1c4-3)
LLL-----------RRR
 LLL-----------RR
  LLL-----------R
   LLL--------RRR  (приложение догоняет)

Посмотрите, как это выглядит для пользователя:

  • L, кажется, движется очень плавно с постоянной скоростью в одном направлении, так что это на самом деле хорошо, но

  • Просто посмотрите, что делает R:

    RRR
     RR
      р
    RRR (приложение догоняет)
    
    • R сначала скользит вправо на два столбца. Левый край R, кажется, движется вправо вместе с остальной частью R.
    • Затем R снова возвращается влево.

Как вы теперь должны знать после прочтения раздела 1c3, это движение назад и вперед выглядит абсолютно ужасно и намного хуже, чем, по общему признанию, странное поведение на рис. 1c4-1 и рис. 1c4-2.

1c5. Подождите немного, затем попробуйте один из вышеперечисленных

Пока что мы представили отдельные идеи о том, что делать, когда пользователь начал перетаскивать границы окна, но приложение еще не перерисовывалось.

Эти методы могут быть объединены.

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

  • если ваше приложение будет реагировать быстро, то любые изменения, которые Microsoft вносит в экран, заставят ваше приложение выглядеть хуже, чем если бы Microsoft просто позволяла вам рисовать реальный контент (помните, что все уловки, приведенные выше, странны в различной степени и ваш контент выглядит странно, поэтому лучше не использовать какие-либо из этих уловок).

  • но если Microsoft будет ждать, пока вы начнете рисовать слишком долго, ваше приложение (и, соответственно, Windows) будет выглядеть зависшим и не отвечающим, как мы объясняли в Разделе 1c1. Это заставляет Microsoft терять лицо, даже если это ваша вина.

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

Это звучит ужасно и хакерски для вас? Угадай, что? Это то, что делает Windows, по крайней мере, двумя разными способами одновременно с двумя разными сроками. ЧАСТЬ 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 Как избежать временных изменений в изменении размера графического интерфейса?: упоминает хитрость перехвата WM_WINDOWPOSCHANGING и настройка WINDOWPOS.flags |= SWP_NOCOPYBITS,

Отчет об ошибке Unity за 2016 год: "Изменение размера окна очень изменчиво и заикается (граница не плавно следует за мышью)" - типичный отчет об ошибках, обнаруженный в сотнях приложений, который частично связан с проблемой в этом отчете об ошибках, а частично из-за того, что некоторые приложения имеют медленное рисование. Единственный документ, который я когда-либо нашел, который на самом деле говорит, что Windows 10 DWM зажимает и расширяет внешний пиксель старого окна, что я могу подтвердить.

2014 Мерцание в окне при изменении размера с левой стороны с ответом до Windows-8, включая CS_HREDRAW/CS_VREDRAW а также WM_NCCALCSIZE,

Окно изменения размера 2013 вызывает размытие около правой границы с помощью старой версии Win-7, которая отключает Aero.

2018 Расширение (изменение размера) окна влево без мерцания - пример случая с несколькими окнами (multi-HWND), реального ответа нет.

WinAPI C++ 2013 : изменение размера окна перепрограммирования: слишком неоднозначно задан вопрос, идет ли речь о мерцании клиентской области (как этот вопрос) или мерцании вне клиентской области.

Ошибка GLFW 2018 года "Изменение размеров окон в Windows 10 демонстрирует скачкообразное поведение" - одна из МНОГИХ таких ошибок, которые никогда не объясняют контекст, как многие сообщения Stackru

2008 "Изменение размера основного кадра без мерцания" CodeProject, который на самом деле выполняет StretchBlt, но не работает в мире Windows 8+, где приложение не может контролировать отображение неправильных пикселей на экране.

2014 Плавное изменение размеров окна в Windows (с использованием Direct2D 1.1)?: Четко заявленная, но оставшаяся без ответа проблема с копией DWM в Windows 8+

2010 Как заставить Windows НЕ перерисовывать что-либо в моем диалоге, когда пользователь изменяет размер моего диалога? Исправлено: WM_NCCALCSIZE, чтобы отключить битовый бит, который больше не работает в Windows 8+, поскольку DWM повреждает экран, прежде чем приложение сможет его отобразить.

2014 Мерцание при перемещении / изменении размера окна: сводка предыдущих исправлений, которые не работают в Windows 8+.

CodeProject 2007 года эпохи WinXP "уменьшающий мерцание", рекомендующий WM_ERASEBKGND+SWP_NOCOPYBITS

Ранний Google Bug 2008 сообщает о новых проблемах в Vista DWM

Если вы используете 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);
    }
}

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

Прежде всего, большое спасибо за то, что собрали полезную информацию в одном месте, но, к сожалению, Microsoft, похоже, это не волнует. Ненавижу цитировать Стива Джобса по этому поводу:

Единственная проблема Microsoft в том, что у них просто нет вкуса. У них совершенно нет вкуса

Я создаю программу WinForms с использованием C#, все, что мне нужно, это иметь панель фиксированного размера, которая все время остается в правом верхнем углу окна. Если я просто использую панель и устанавливаю ее свойство Anchor/Dock, я получу ужасное дрожание при изменении размера, от которого страдает почти каждая отдельная программа Windows.

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

  • Панель в правой части окна имеет фиксированный размер, перерисовывать не нужно = разрывов нет.

  • я не ставлю никакихButtonsили другие стандартные интерактивные элементы управления на нем. Они активируют второе окно, и мне не удалось найти способ предотвратить это. (MouseDownсобытие в моем пользовательском элементе управления работает без активации.)

Обходной путь:

  1. Создайте файл, поместите на него все, кроме фиксированной панели.
  2. Создайте фиксированную панель с помощью:
          protected override bool ShowWithoutActivation => true;

Оформите его так, чтобы он вписывался в (набор)FormBorderStyleкNone,ShowInTaskbarкFalse, и т. д...)


  1. Инициализируйте входBaseForm_Shownсобытие, где я могу вычислить разницу положения от верхнего правого угла (включая рамку окна, а не клиентскую область) до верхнего левого угла файла . (все в экранных координатах)
          private FloatForm _floatForm;
    private int _floatFormXtoRight;
    private int _floatFormYtoTop;

    private void BaseForm_Shown(object sender, EventArgs e)
    {
        _floatForm = new FloatForm();
        _floatForm.Location = PointToScreen(ClientRectangle.Location);
        _floatForm.Left += ClientRectangle.Width - _floatForm.Width;
        _floatFormXtoRight = Right - _floatForm.Right + _floatForm.Width;
        _floatFormYtoTop = _floatForm.Top - Top;
        _floatForm.Show(this);
    }

  1. РучкаWM_WINDOWPOSCHANGINGсообщение вBaseForm, рассчитаем и установим новое местоположение базы по полученному вm.LParam(эти значения вWINDOWPOSвключить оконную рамку, поэтому был необходим расчет дельты на последнем шаге)
          [StructLayout(LayoutKind.Sequential)]
    public struct WINDOWPOS
    {
        public IntPtr hwnd;
        public IntPtr hwndInsertAfter;
        public int x, y, cx, cy;
        public uint flags;
    }

    protected override void WndProc(ref Message m)
    {

        const int WM_WINDOWPOSCHANGING = 0x0046;

        switch (m.Msg) {
            case WM_WINDOWPOSCHANGING:
                var w = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
                //Console.WriteLine($"WM_WINDOWPOSCHANGING: x{w.x} y{w.y} w{w.cx} h{w.cy}");

                if (_floatForm != null && w.cx != 0) {
                    _floatForm.Left = w.x + w.cx - _floatFormXtoRight;
                    _floatForm.Top = w.y + _floatFormYtoTop;
                }
                break;
        }
        base.WndProc(ref m);
    }

Я потратил так много времени здесь, пытаясь справитьсяWM_NCCALCSIZEи вернутьсяWVR_VALIDRECTSс моим собственнымNCCALCSIZE_PARAMSбезуспешно... что побудило меня создать этот обходной путь.


  1. РучкаWM_MOUSEACTIVATEсообщение вFloatFormвернутьсяMA_NOACTIVATEпоэтому он не активируется при нажатии (но событие мыши, напримерMouseEnterибо элементы управления все равно будут работать)
          protected override void WndProc(ref Message m)
    {
        const int WM_MOUSEACTIVATE = 0x0021;

        switch (m.Msg) {
            case WM_MOUSEACTIVATE:
                const int MA_NOACTIVATE = 3;

                m.Result = (IntPtr)MA_NOACTIVATE;
                return;
        }
        base.WndProc(ref m);
    }

Результат:

(значок «лампочка» взят из Led Icons Pack от Led24)

Это самый простой обходной путь, который я могу придумать. Есть еще некоторые детали, которые необходимо проработать (например,FloatWindowвсплывет раньше, чемBaseWindowзавершить восстановление из минимизированной анимации), но это выходит за рамки данной темы.

Другие вопросы по тегам