Как правильно предварительно переключиться на курсор ожидания в Win32? (Или: как правильно запустить WM_SETCURSOR?)

Вот проблема, с которой я столкнулся:

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

  • SetCursor действует только до движения мыши.

  • WM_SETCURSOR действует только после перемещения мыши.

Вы могли подумать, что я мог бы просто сделать оба из вышеперечисленного, одновременно вызывая SetCursor и изменяя поведение WM_SETCURSOR, так что я заставляю курсор менять до некоторого момента в будущем.

Но я не могу просто так сделать. Почему? Поскольку SetCursor применяется ко всему приложению, случайное окно не знает (да и не должно) знать правильный курсор во всем приложении. Чтобы отправить WM_SETCURSOR окну, которое на самом деле находится под курсором, необходимо выполнить соответствующие проверки попадания, и неясно, как это сделать правильно.

Как правильно переключать курсоры и обратно в Win32? Каждый пример, который я вижу, тривиален и игнорирует эти проблемы.


person user541686    schedule 23.02.2021    source источник
comment
Все, что вам нужно, - это приложение. Используйте GetCursor (), SetCursor (), медленные вещи, которые вешают цикл сообщений, SetCursor () для восстановления. Если медленный материал на самом деле не вешает цикл сообщений (это непонятно), тогда он становится намного сложнее, поскольку все дочерние окна должны взаимодействовать, чтобы сохранить песочные часы. Обычно практично только в рамках, по сравнению с Winform Control.UseWaitCursor.   -  person Hans Passant    schedule 24.02.2021
comment
@HansPassant: Даже если я сделаю это для всего приложения, курсор все равно должен оставаться незатронутым, когда я (скажем) изменяю размер границы или нахожусь над полем редактирования, иначе это сбивает с толку. И просто не имеет смысла, что движение на 1 пиксель внезапно вызовет изменение, когда операция все еще выполняется, поэтому у меня все еще есть эта проблема, даже если я готов немедленно вызвать SetCursor, чтобы изменить ее во всем приложении. Ничто здесь не останавливает цикл сообщений - это фоновая операция в другом потоке, который запускается / останавливается основным потоком, - но как люди заставляют это работать?   -  person user541686    schedule 24.02.2021
comment
Они этого не делают, всплывающее диалоговое окно работы над ним - стандартный подход.   -  person Hans Passant    schedule 24.02.2021
comment
@HansPassant: Забавно, что вы упомянули об этом, это именно то, от чего я пытаюсь избавиться, так как это неудобно для пользователя и ненужно блокировать пользовательский интерфейс или открывать дополнительное окно для того, что я делаю. Так действительно нет хорошего решения? :( Если да, не стесняйтесь публиковать это как ответ ...   -  person user541686    schedule 24.02.2021
comment
Он становится более дружелюбным за счет явного отключения элементов управления, которые нельзя безопасно использовать, когда рабочий поток отторгает. Как и кнопка, с которой он был запущен.   -  person Hans Passant    schedule 24.02.2021
comment
@HansPassant: Я добавляю в список, и все в этом списке можно использовать, пока оно добавляется. заголовки могут не использоваться, но это все. Я могу придумать и другие идеи, но для целей вопроса мне просто интересно, что я могу сделать с курсором.   -  person user541686    schedule 24.02.2021
comment
Я не понимаю, что плохого в том, чтобы позвонить SetCursor. он обеспечивает немедленную обратную связь. Если пользователь наводит указатель мыши на что-то, что не отключено, курсор снова возвращается в нормальное состояние. Пользователь привык к тому, что курсор меняется при его перемещении, поэтому я не вижу в этом проблемы.   -  person Jonathan Potter    schedule 24.02.2021
comment
@JonathanPotter: С этим как минимум 2 проблемы. Во-первых, если они, например, изменяя размер окна, курсор не должен внезапно меняться, точка. Во-вторых, даже если я все равно это сделаю, на что я верну курсор после завершения операции? Это будет полный контроль, и я понятия не имею, какой курсор подходит для этого элемента управления, поэтому я не могу просто восстановить оригинал. (Представьте, если бы я восстановил двутавровую балку, когда она находится над списком ...) Что мне действительно нужно сделать, так это запустить WM_SETCURSOR для всего, что находится под курсором, а затем он определит курсор.   -  person user541686    schedule 24.02.2021
comment
Действительно ли вероятна первая проблема? Было бы необычно, если бы операция блокировки началась, когда пользователь делал что-то модальное, например, изменение размера окна. Что касается второй проблемы, я не думаю, что есть какая-то причина, по которой вы не можете самостоятельно отправить WM_SETCURSOR сообщение окну под курсором.   -  person Jonathan Potter    schedule 24.02.2021
comment
@JonathanPotter: Да, вполне вероятно. Это маловероятно, если представить себе, что операции блокируются и долго. У меня есть длинные и короткие окна в фоновом режиме, и нет причин, по которым пользователь не может изменить размер окна в течение этого времени. Не то чтобы проблема была бы иной, даже если бы пользователь зависал над границей и не менял размер активно. Что касается второго, то это то, для чего я пишу код прямо сейчас (вызываю WindowFromPoint и выполняю проверку попадания, и все это вручную ...); Я пытался ответить на ваш вопрос, почему вы не должны слепо звонить SetCursor.   -  person user541686    schedule 24.02.2021
comment
Я как бы согласен с Хансом в том, что курсор может быть не лучшим способом указать активность, учитывая, что он будет указывать на фоновую активность, которую пользователь не инициировал напрямую. Может быть, какая-нибудь графика в углу могла бы быть лучше. В любом случае, дизайн вашего приложения зависит от вас :) Но я не уверен, что есть простой способ проверить, является ли курсор в настоящее время стандартной стрелкой (и, следовательно, его можно безопасно изменить), что звучит как недостающий элемент вашей головоломки.   -  person Jonathan Potter    schedule 24.02.2021
comment
@JonathanPotter: У меня будут и другие индикаторы, но они не заменяют друг друга в каждой ситуации, и я не хочу обсуждать это здесь. На самом деле это сложнее, чем вы думаете, потому что, даже если это не стрелка, иногда мне все равно нужно ее изменить. (например, IDC_APPSTARTING тоже должен измениться на IDC_WAIT.) И, учитывая, что несколько операций могут завершиться не по порядку, выяснить, к чему вернуться, не очевидно. Я пишу для этого код, и хотя у меня что-то есть, оно по своей сути нетривиально, склонно к гонкам и неполно, и кажется, что WM_SETCURSOR не обрабатывается последовательно.   -  person user541686    schedule 24.02.2021
comment
Это действительно кажется сложной проблемой, требующей совместной обработки курсоров для всех элементов управления в программе. Короче говоря, не стесняйтесь обновлять свою ветку, если есть какие-либо успехи.   -  person Strive Sun    schedule 24.02.2021
comment
@ StriveSun-MSFT: Да ... меня шокирует то, почему, насколько я могу судить, в Интернете нет никаких намеков на обсуждение этого вопроса. Я бы подумал, что это настолько известная проблема, что у нее будет четкое решение в сообществе ... конечно же, я не первый человек, который достаточно заботится о том, чтобы решить эту проблему после почти трех десятилетий ?! Изменения курсора настолько распространены, что все, что я могу себе представить, - это то, что каждый, кому было небезразлично, работал над нишевой или коммерческой программой и нигде не спрашивал на форуме. И отчасти это также частный случай более общей проблемы с переключением когерентного состояния.   -  person user541686    schedule 24.02.2021
comment
Просто чтобы исключить последнюю двусмысленную формулировку, правильно ли я это понимаю? Вы хотите, чтобы в всех возможных случаях курсор определялся исключительно конкретным окном, которое находится непосредственно под курсором (например, определенной кнопкой)? Другими словами, вы всегда точно знаете, что делать в ваших WM_SETCURSOR сообщениях (и вы успешно реализовали это), но если вы когда-нибудь захотите установить курсор вне WM_SETCURSOR, вы не знаете, каким должен быть курсор на самом деле, потому что теперь у вас нет такого же контроля, как у того, где сейчас находится мышь?   -  person dialer    schedule 24.02.2021
comment
@dialer: Я так думаю? Хотя в настоящее время я пишу код не для элемента управления, а для всего окна / диалогового окна (хотя мне, возможно, придется это сделать), и все же я думаю, что имеет значение, что находится под курсором (например, IMHO, кнопка и поле редактирования должно вызывать разные ответы), и я даже не знаю, существует ли соглашение о том, что должно иметь приоритет и еще много чего, поэтому я сам придумываю правила приоритизации на лету. И я пытаюсь сгенерировать и отправить WM_SETCURSOR вручную, но он работает нестабильно, и я все еще пытаюсь отладить, связана ли ошибка с этим или нет.   -  person user541686    schedule 24.02.2021
comment
@dialer: На высоком уровне мой вопрос не обязательно в том, каков код для этой конкретной операции, а скорее как люди решают эту проблему установки курсора в целом, потому что, возможно, я просто пытаюсь выполнить неправильные операции с самого начала, и есть устоявшаяся практика последовательного изменения курсоров, я не знаю.   -  person user541686    schedule 24.02.2021
comment
Вот что я имел в виду. Я думаю, что причина, по которой это кажется сложным, заключается в том, что это будет настолько сложно, насколько вы этого хотите. Многие разработчики не обращают особого внимания на такие детали и никогда тщательно не проверяли правильность курсора во всех возможных случаях. В конце концов, он исправится через WM_SETCURSOR сообщений. Они, вероятно, никогда не звонят SetCursor за пределами WM_SETCURSOR. Конечный пользователь заметит это, только если будет пристально смотреть на курсор, не перемещая его. API тоже не может сильно помочь, так как не знает, насколько сложным вы решили быть в вашем случае.   -  person dialer    schedule 24.02.2021
comment
Похоже, у вас уже есть простое решение. Уже существует своего рода система приоритетов: тот факт, что сообщение WM_SETCURSOR сначала отправляется самому внутреннему элементу управления. Если этот элемент управления не имеет твердого мнения о том, каким должен быть курсор, он может запросить у своего родителя выбор с более низким приоритетом (фактически, это то, что делает DefWindowProc). GetCursorPos + WindowFromPoint + Отправка WM_SETCURSOR всякий раз, когда вы считаете, что состояние курсора могло измениться, должно позаботиться обо всем остальном. Если это у вас глючит, то, может быть, это другой вопрос.   -  person dialer    schedule 25.02.2021
comment
(продолжение) Вам необходимо принять во внимание захваченное окно (GetCapture).   -  person dialer    schedule 25.02.2021
comment
@dialer: Я говорю не о системе приоритетов, о которой вы говорите. Я говорю о приоритете между курсорами, вы говорите о приоритете между элементами управления. У меня могут быть две задачи, которые диктуют разные курсоры для одного и того же окна; какой из них выбрать (особенно если они заканчиваются не по порядку) нетривиально и требует явного отслеживания приоритетов. Я знаю, что теоретически это должно быть так же просто, как отправить WM_SETCURSOR, но на самом деле все не так просто. Порядок, а также SendMessage по сравнению с SendMessageCallback имеет значение. Я все это открываю прямо сейчас.   -  person user541686    schedule 25.02.2021
comment
@dialer: И я тестирую изменения курсора из других потоков, и сообщения не обязательно даже перекачиваются, когда я ожидаю, что они будут (и наоборот!), поэтому изменения курсора не происходят, когда я ожидаю, и у меня есть чтобы выяснить почему. Блокировка также имеет значение для производительности. Если бы я знал, что было ошибкой и что было фундаментальным ограничением, которое могло бы потребовать от меня изменения моего подхода, я бы не спрашивал здесь о каноническом решении. Я не могу описать, насколько неочевидно на самом деле понять, что же на самом деле происходит ... вам просто нужно попробовать этот подход в приложении, отличном от игрушек, чтобы понять, что я имею в виду.   -  person user541686    schedule 25.02.2021
comment
Нет, это именно то, что я имею в виду под , это будет так же сложно, как вы решите. Если у вас есть окно, которое может иметь одно из нескольких различных состояний курсора (независимо от того, находится оно на верхнем уровне или нет), то в любой момент времени это окно должно быть готово к получению WM_SETCURSOR и (быстро) уметь определять, какие курсор должен быть. Код, который там находится, не имеет абсолютно ничего общего с winapi или какими-либо предполагаемыми сложностями. Это может быть просто if (data->Prio1TaskRunning) SetCursor(A); else if (data->Prio2TaskRunning) SetCursor(B);. (продолжение ...)   -  person dialer    schedule 25.02.2021
comment
(продолжение) Данные, необходимые для этого решения, должны быть легко доступны потоку пользовательского интерфейса. Если это не так, то синхронизация состояния вашего приложения нарушена. Что касается потоков, если вы управляете пользовательским интерфейсом из нескольких потоков, вы делаете это неправильно. Многие API имеют скрытые сходства потоков, и вы будете < / i> зарыться в яму. Только ваш поток пользовательского интерфейса отвечает за обновление состояния пользовательского интерфейса. Если другой поток делает что-то, что может потребовать обновления пользовательского интерфейса, он должен сначала обновить общие данные, а затем (продолжение ...)   -  person dialer    schedule 25.02.2021
comment
(продолжение) публиковать (не отправлять) сообщение и позволить потоку пользовательского интерфейса разобраться. И да, сообщения не обязательно обрабатываются по порядку по нескольким причинам: вот один из них. По поводу приоритетов: я понял, о чем вы, но вы меня не поняли. Система дает вам все необходимое. Когда курсор находится над элементом управления, этот элемент управления может устанавливать курсор. Или он может переслать его своему родителю. Он может даже спросить своих родителей, какой курсор наивысшего приоритета у его предков (рекурсивно), и принять решение на основе этого.   -  person dialer    schedule 25.02.2021
comment
@dialer: Я только что отправил ответ.   -  person user541686    schedule 25.02.2021
comment
@Strive Sun - MSFT: Я опубликовал ответ, если вам интересно.   -  person user541686    schedule 25.02.2021


Ответы (2)


1_ Реализовать курсор занятости проще всего, когда он сочетается с отключенным вводом. Сначала отключите вход EnableWindow( hwnd, FALSE );, а затем установите курсор занятости SetCursor( LoadCursor( 0, IDC_WAIT ) );. Теперь вы можете выполнить некоторую операцию (в идеале не дольше 5 секунд). После этого разрешите вход EnableWindow( hwnd, TRUE );. Когда ваша операция длится более 5 секунд, окно будет призрачным, поэтому он потеряет занятый курсор в строке заголовка и границах изменения размера.

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

В оконной процедуре создания подкласса просто установите курсор занятости и верните TRUE. Не звоните DefWindowProc или DefSubclassProc в случае WM_SETCURSOR.

switch ( message )
{
case WM_SETCURSOR:
    SetCursor( MyCursor );
    return TRUE;
...
}

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

Интересно, что это работает даже для меню и полей со списком со всплывающими списками.

3_ Другой вариант - скрыть курсор ShowCursor( FALSE ); и отобразить вместо него полупрозрачное окно, отслеживающее положение курсора мыши с некоторой возможностью перехода по щелчку. Лично я бы начал с окна, отображаемого всего на несколько пикселей выше или ниже текущей позиции курсора.

Может быть, будет проще индикатор выполнения в строке состояния или простая анимация (песочные часы?) В главном окне.

person Daniel Sęk    schedule 24.02.2021
comment
Я этого не делаю и не могу, к сожалению, вся цель - оставить приложение работоспособным, пока я выполняю операцию. Тем не менее, спасибо за попытку. - person user541686; 24.02.2021
comment
Не работает, WM_SETCURSOR отправляется, а не публикуется. Так что перехватить можно только в оконной процедуре. - person Hans Passant; 24.02.2021
comment
@HansPassant Вы правы. Я проверил свой старый исходный код, и настраиваемая структура автоматически подклассифицирует все элементы управления, поэтому if ( msg == WM_SETCURSOR ) находится в одной единственной процедуре, а не в цикле сообщений. К сожалению, этот ответ неверен. - person Daniel Sęk; 24.02.2021
comment
@ user541686 Я изменил свой ответ, чтобы он мог быть более полезным. Я проверил варианты 1, 2 и 3. По крайней мере, в Windows 10 они работают. - person Daniel Sęk; 24.02.2021
comment
@ DanielSęk: Я не знаю, нужно ли мне идти по пути подкласса, но это похоже на возможность. Спасибо за обновления. +1 - person user541686; 24.02.2021

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

Ваш опыт может варьироваться в зависимости от сложности вашего приложения.

Вот что я бы посоветовал сделать кому-нибудь в подобной ситуации:

  1. Определите сообщение для всего приложения, например WM_UPDATECURSOR = WM_APP + 0.

  2. Попросите ваш основной (GUI) поток обрабатывать WM_UPDATECURSOR, запустив WM_SETCURSOR:

    а. Используйте GetCursorPos и WindowFromPoint, чтобы поместить поток под курсором.

    б. Используйте GetWindowThreadProcessId для проверки идентификаторов процессов и потоков.

    c. Если это другой процесс, остановитесь.

    d. Если он находится в потоке, отличном от вашего процесса, PostThreadMessage(thread_id, WM_UPDATECURSOR) к нему, остановитесь. (Это в высшей степени необычная и в целом плохая практика; я упоминаю ее только для полноты картины.)

    е. Если это для вашего собственного потока, используйте WM_NCHITTEST, и если это удастся, выясните (как объясняется ниже), каким должен быть правильный курсор, если что-либо. Если что, установите его с помощью SetCursor; в противном случае SendMessage(WM_SETCURSOR).

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

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

  5. Чтобы выяснить, находится ли курсор в какой-либо точке, просмотрите список в обратном порядке и найдите курсор, соответствующий первому положительному счетчику. Если нет, то верните NULL; нет сложенного курсора.

  6. В обработчике WM_SETCURSOR главного окна / диалогового окна оцените курсор из глобального списка, как описано в предыдущем шаге, и используйте SetCursor, чтобы установить его. Однако верните FALSE, чтобы дочерние окна (например, элементы управления Edit) по-прежнему переопределяли его, если захотят.

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

person user541686    schedule 24.02.2021
comment
Как упоминалось ранее, при захвате мыши Windows перестает отправлять WM_SETCURSOR сообщения. Непосредственным следствием этого является то, что если мышь перемещается за пределы окна захвата, пока она все еще захватывается, курсор мыши по-прежнему ведет себя так же, как и во время захвата внутри окна. В этом случае запрос курсора из окна под положением мыши приведет к другому поведению. - person dialer; 25.02.2021
comment
@dialer: Как уже упоминалось в ответе, он не идеален и все еще имеет неровности ... - person user541686; 25.02.2021