OberonCore
https://forum.oberoncore.ru/

Проблема Ports.Rider.Input
https://forum.oberoncore.ru/viewtopic.php?f=24&t=370
Страница 1 из 1

Автор:  Александр Ильин [ Среда, 24 Январь, 2007 16:58 ]
Заголовок сообщения:  Проблема Ports.Rider.Input

Многие ветераны программирования в BlackBox Component Builder знакомы с целым комплексом проблем, вызываемых самим наличием процедуры Ports.Rider.Input, не говоря уже о последствиях активного использования ее разработчиками при написании фреймворка. Предлагаю обсудить здесь достоинства и недостатки данного проектного решения, а заодно и предложить альтернативы.

Пробему вызывают многочисленные циклы следующего вида:
Код:
REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown;

Во время работы подобного цикла загрузка процессора вырастает до 100%, не работают фоновые задачи типа Services.Action и не производится обработка оконной очереди сообщений Windows.

Зачем нужны такие циклы, какую задачу они решают? Дело в том, что подобный обработчик вынужден использовать всякий View, который хочет дождаться отпускания кнопки мыши пользователем. А такая задача возникает повсеместно. От банального нажатия кнопкоподобного элемента интерфейса (ведь если отпустить кнопку мыши за пределами элемента, нажатие не должно иметь эффекта) до выделения блока текста. Последний пример наглядно демонстрирует, что мы имеем дело с режимом в интерфейсе. Как известно, режимов должно быть как можно меньше и всякий режим должен быть известен пользователю. В данном случае пользователь знает о режиме ровно настолько, насколько он знает, что удерживает кнопку мыши в нажатом состоянии. Обычному пользователю этого достаточно, так что проблема не в самом наличии режима, а в его реализации.

Возможны ли альтернативные реализации данного режима, столь необходимого в нашей повседневной работе? Послушаем мнение специалистов...

Штирлиц писал(а):
Обычно обработку реализуют так (схема):

WM_LBUTTONDOWN:
SetCapture; (подготовка к обработке)
WM_MOUSEMOVE:
(обработка, например выделяем текст)
WM_MOUSEUP:
ReleaseCapture;(конец обработки)

Для этого у нас почти все есть
Controllers.TrackMsg - возникает когда нажали мышь
Controllers.PollCursorMsg - возникает при движении мыши
ввести еще сообщение типа(по крайней мере в документации я не нашел чего-то подобного)
Controllers.MouseUpMsg возникает когда мышь отпущена
И тогда не надо будет писать
REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown;

Если такой способ обработки в приложении остается на совести программиста, то из ядра Blackbox такие вещи надо выкинуть однозначно (если это возможно). Необходимо критически пересмотреть обработку сообщений в ядре Blackbox. Тогда и процессор не будет загружаться на 100% и другие задачи замирать не будут, только от того что нажата мышь.
Ввести все-таки функцию (например, включить ее в модуль System\Windows) типа PROCEDURE ProcessMessages: BOOLEAN,
которую программист мог-бы вызывать на свое усмотрение (все зависит от задачи которую он решает).
переносимость не пострадает, потому как на других платформах ProcessMessages может быть пустышкой.


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

К сожалению или к счастью, но аналогичного механизма не было заложено при разработке фреймворка BlackBox. Windows API нам может помочь только лишь привязать сообщения мыши к одному из окон BlackBox, но не к конкретному View внутри окна. За доставку сообщений до конкретного View отвечают процедуры с именем ForwardCtrlMsg (модули Views, Windows, HostWindows). Эти процедуры отправляют сообщения типа Views.CtrlMessage тому View, который в настоящий момент находится в фокусе ввода. View получает и обрабатывает эти сообщения в методе HandleCtrlMsg. Сообщения об изменении состояния кнопок мыши приходят в виде типа Controllers.TrackMsg, а об изменении положения курсора - в виде Controllers.PollCursorMsg. Нет нужды в особом сообщении типа MouseUpMsg, поскольку объект, если его интересует нажатость некоторой кнопки, может запомнить факт нажатия в обработчике Controllers.TrackMsg и дожидаться отпускания (т.е. пропадания соответствующего флага из набора Controllers.TrackMsg.modifiers) в этом же обработчике.

Таким образом, все необходимые сообщения у нас есть, нет только уверенности, что они придут к нам в полном объеме. Предполагаю, что проблемы появятся в двух случаях:
1) если нажать кнопку мыши в сфокусированном View, а затем увести курсор мыши за пределы содержащего его окна;
2) если вывести курсор мыши за пределы View c так называемым "горячим фокусом".
Оба эти варианта лучше проверить экспериментально, создав соответствующие тестовые View без цикла опроса Input.

В первом случае проблема будет вызвана отсутствием захвата мыши окном на уровне Windows API (с помощью SetCapture). Во втором случае проблема будет вызвана тем, что View с "горячим фокусом" не оповещается об уводе курсора за его пределы, а фокус приобретается только на время обработки сообщения о нажатии кнопки мыши. Оба эти случая, повторяю, требуют проверки и являются на настоящий момент моими домыслами. Естественно, возможны и другие "подводные камни", о которых я пока не догадываюсь.

Что касается преимуществ циклов с Rider.Input, то самые очевидные из них таковы:
1. отсутствие дополнительной сущности "режим захвата мыши" в устройстве фреймворка;
2. простота реализации кода без разбивки на три части: "вход в режим", "работа в режиме" и "выход из режима";
3. гарантия целостности данных и объектов, связанная с отсутствием разрывов в обработке. Например, можно быть уверенным, что в процессе выделения блока текста этот самый блок не будет модифицирован или удален вклинившейся фоновой задачей Services.Action или каким-то иным образом в процессе обработки оконной очереди сообщений Windows.

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

Что скажете, господа?

Автор:  Штирлиц [ Среда, 24 Январь, 2007 18:53 ]
Заголовок сообщения: 

Ничего страшного в нет в том, что процесс разбит на три части:
"вход в режим", "работа в режиме" и "выход из режима";

Вы посмотрите как реализована обработка:

| USER32.WMLButtonDown, USER32.WMRButtonDown, USER32.WMMButtonDown:
IF wnd # USER32.GetFocus() THEN
res := USER32.SendMessageA(client, USER32.WMMDIActivate, wnd, 0);
res := USER32.SetFocus(wnd)
END;
(*мышь мы захватили, никуда она от нас не денется*)
res := USER32.SetCapture(wnd);
Kernel.Try(HandleMouse, wnd, wParam + 256, lParam);
(*следующую строчку по хорошему убрать*)
IF USER32.GetCapture() = wnd THEN res := USER32.ReleaseCapture() END;
Controllers.ResetCurrentPath();
RETURN 0
потом для view в HandleMouse генерируется:
Controllers.TrackMsg

а у же в обработчике вызываем Input

PROCEDURE (rd: Rider) Input* (OUT x, y: INTEGER; OUT modifiers: SET; OUT isDown: BOOLEAN);
VAR msg: USER32.Message; wnd, mw: USER32.Handle; pt: USER32.Point; res: INTEGER; set: SET;
BEGIN
wnd := rd.port.wnd; mw := USER32.GetCapture();
(*USER32.PeekMessageA тоже можно убрать*)
IF USER32.PeekMessageA(msg, mw, USER32.WMMouseMove, USER32.WMMButtonDblClk, 1) # 0 THEN
mx := (msg.lParam + 32768) MOD 65536 - 32768; my := msg.lParam DIV 65536;
IF (mw # 0) & (wnd # mw) THEN
pt.x := mx; pt.y := my; res := USER32.ClientToScreen(mw, pt);
res := USER32.ScreenToClient(wnd, pt); mx := pt.x; my := pt.y
END;
mb := {};
....
обработали.

Потом так же вызываем Input при движении мыши.

по
| USER32.WMLButtonUp, USER32.WMRButtonUp, USER32.WMMButtonUp:
Kernel.Try(HandleMouse, wnd, wParam, lParam);
(* освобождаем захват*)
IF USER32.GetCapture() = wnd THEN res := USER32.ReleaseCapture() END;
Controllers.ResetCurrentPath();

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

Зато нет REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; как-будто какой-то фоновой процесс возьмет да и сопрет мышонка.
Фоновые задачи они и должны выполнять другие функции, а не воровать мышь.

А так получается:
1. Нет нагрузки на процессор.
2. Очередь сообщений работает.
3. Взаимодействие с пользователем проходит нормально.
4. Переносимость от этого не страдает.

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

Автор:  Александр Ильин [ Среда, 24 Январь, 2007 21:49 ]
Заголовок сообщения: 

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

Познакомьтесь, пожалуйста, с процедурой Views.ForwardCtrlMsg и механизмом определения сфокусированного View. Потом посмотрите документацию про Properties.FocusPref.hotFocus. Поэксперементируйте, я же просил. Вы поймете, что SetCapture далеко не достаточно для того, чтобы оповещать View об отпущенной кнопке мыши. В том-то и проблема, что фреймворк не знает, кого оповещать. Не предусмотрено такое знание.

Если мне не верите, попробуйте перевести на раздельный режим работы хотя бы тот же DevCommanders.StdView (он использует hotFocus). Вы увидите, что никакие сообщения TrackMsg или PollCursorMsg не приходят к View, если курсор мыши не находится непосредственно над ним.

То, что вы написали по поводу SetCapture/ReleaseCaputre, совершенно верно. И это действительно необходимо будет сделать в том или ином виде. Но это только API-шная, внешняя сторона дела. Я же вам толкую о том, что внутреннее устройство фреймворка BlackBox не содержит необходимых механизмов для реализации функций, подобных API-шным SetCapture/ReleaseCaputre. Окно Windows с его процедурой обработки сообщений - это одно, а Views.View с его HandleCtrlMsg - это совсем, совсем другое.
Штирлиц писал(а):
Ничего страшного в нет в том, что процесс разбит на три части: "вход в режим", "работа в режиме" и "выход из режима"
...
Тысячи приложений под Windows (думаю и под Unix все обстоит примерно так же) работают с мышью по схеме "вход в режим", "работа в режиме" и "выход из режима" и ничего. Никто у них мышь при этом не вырывает.

Переписать многое придется, но хорошо, что это вас не пугает. Хотя лично мне, немного полазившему по подсистеме Text, не очень хочется там что-то усложнять. Меня беспокоит то, что поскольку состояние, например, выделения блока текста мышью, является режимом, то это состояние нужно где-то помнить между обработками сообщений мыши. И при этом еще надо иметь в виду, что все обычные операции по правке текста могут в этот режим вмешаться. Именно это я имел в виду под угрозой фоновых задач, а не какое-то непонятное "воровство мыши". Опишу подробнее.

Возьмем для примера простой чат. Пусть, есть фоновая задача, получающая данные из COM-порта или из локальной сети. Эта зачада при получении данных или команд производит соответствующие операции над текстовым документом - например, вывод полученного от удаленного пользователя сообщения. Представьте, что вы начали выделять блок текста в этом самом документе. Вы нажали кнопку мыши и объект запомнил номер символа, с которого начато выделение (пусть это будет 15-й символ). Вы переместили мышь и размер выделения стал равен 5 символам (с 15 по 19-й включительно). Документ перерисовался, отобразив указанные символы выделенными. Тут фоновая задача чата получила команду от удаленного пользователя: очистить текущий документ. Затем вы продолжаете перемещать мышь, выделяя 20-й символ. Документ пытается перерисоваться с выделенными символами с 15 по 20 и выходит за пределы доступного текста. Вы видите сообщение об ошибке.

Что демонстрирует данный гипотетический пример? То, что мало хранить переменные состояния режима в отдельном месте. Нужно еще и обеспечить адекватную реакцию этого самого режима на все возможные раздражители. Другими словами, вы должны досконально знать объект и все возможные приемы и способы работы с ним, чтобы добавлять в него новый режим, не добавляя ошибок. Для DevCommanders.StdView это достаточно просто реализовать, поскольку у него только 4 состояния. Для подсистемы Text я бы за такую работу не взялся.

Вот поэтому явный режим в виде плотного цикла с Rider.Input мне кажется куда более простым решением, чем введение полускрытых режимов с нелокальными переменными состояния в сложные объекты. Это мое мнение. Я не говорю, что это невозможно, я просто говорю, что это не тривиальная задача. И тысячи программ для Windows и Unix, написанные с использованием стандартных EDIT-контролов, тут нам ничем не помогут. Фреймворк надо дорабатывать.

Автор:  Александр Ильин [ Четверг, 25 Январь, 2007 13:29 ]
Заголовок сообщения: 

В дополнение к предыдущему посту. Если мы сделаем разбивку на "вход-работа-выход" для текстовых объектов, то проблемы нам смогут создавать даже не какие-то хитрые Services.Action, а просто клавиатура. Ведь если мы в процессе выделения текста свободно выходим в цикл обработки оконных сообщений, то мы будем получать также и сообщения о нажатии клавиш. Это значит, что пользователь сможет одной рукой выделять текст, а другой рукой одновременно его редактировать, удалять и прочее. В общем-то, для пользователя это даже хорошо. Я просто хочу сказать, что проблемы мы получим самого разного плана, даже без введения дополнительных чатов и прочего.

Кстати, поскольку в текущей реализации цикл Rider.Input не вычищает из очереди сообщения от клавиатуры (а мышиные - вычищает), то нажатые во время выделения или перетаскивания текста символы будут "выплюнуты" нам в текст сразу после отпускания кнопки мыши. С точки зрения интерфейса это... сами понимаете, несколько странно, ведь у пользователя нет представления о "буфере сообщений". По крайней мере, в интерфейсе такое понятие отсутствует.

PS: По поводу текстов... Чем черт не шутит, может быть, что при введении режима выделения и прочих там код структурируется и упростится, уберутся повторы. Не знаю.

Автор:  Александр Ильин [ Четверг, 25 Январь, 2007 13:29 ]
Заголовок сообщения: 

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

Автор:  Штирлиц [ Пятница, 02 Февраль, 2007 10:40 ]
Заголовок сообщения: 

Александр Ильин писал(а):
В дополнение к предыдущему посту. Если мы сделаем разбивку на "вход-работа-выход" для текстовых объектов, то проблемы нам смогут создавать даже не какие-то хитрые Services.Action, а просто клавиатура. Ведь если мы в процессе выделения текста свободно выходим в цикл обработки оконных сообщений, то мы будем получать также и сообщения о нажатии клавиш. Это значит, что пользователь сможет одной рукой выделять текст, а другой рукой одновременно его редактировать, удалять и прочее. В общем-то, для пользователя это даже хорошо. Я просто хочу сказать, что проблемы мы получим самого разного плана, даже без введения дополнительных чатов и прочего.


Как реагировать и что делать, когда пользователь нажимает кнопки во время движения мыши полностью зависит от логики разрабатываемого приложения. И проблем я в этом ни каких не вижу. Хотите чтобы у вас пользователь вводил слова во время выделения мыши - ваше дело, не хотите не будет этого.

Автор:  Штирлиц [ Пятница, 02 Февраль, 2007 12:00 ]
Заголовок сообщения: 

Александр Ильин писал(а):
Я немного поэкспериментировал и вот что выяснил. С "горячим фокусом" или с обычным, встроенные View не получают сообщения мыши, если курсор находится не над ними. Сообщения мыши всегда приходят только к тому View, над которым находится курсор. Это означает, что в фреймворке на сегодня нет вообще никакой возможности узнать о выходе мыши за пределы View, кроме как с помощью цикла Rider.Input.


Ну, почему же нельзя, можно, если доработать. Смотрим HostWindows
DocWinHandler:

| USER32.WMLButtonDown, USER32.WMRButtonDown, USER32.WMMButtonDown:
IF wnd # USER32.GetFocus() THEN
res := USER32.SendMessageA(client, USER32.WMMDIActivate, wnd, 0);
res := USER32.SetFocus(wnd)
END;
//Захват мыши
res := USER32.SetCapture(wnd);
Kernel.Try(HandleMouse, wnd, wParam + 256, lParam);
это на до убрать
(*IF USER32.GetCapture() = wnd THEN res := USER32.ReleaseCapture() END;*)
Controllers.ResetCurrentPath();
RETURN 0
| USER32.WMLButtonUp, USER32.WMRButtonUp, USER32.WMMButtonUp:
Kernel.Try(HandleMouse, wnd, wParam, lParam);
освобождаем мышь
IF USER32.GetCapture() = wnd THEN res := USER32.ReleaseCapture() END;
Controllers.ResetCurrentPath();
RETURN 0
| USER32.WMMouseMove:
здесь мы будем получать сообщения от мыши не зависимо от того, за окном она или нет
Kernel.Try(HandleMouse, wnd, wParam + (2 * 256), lParam);
Controllers.ResetCurrentPath();
RETURN 0

Далее можно ввести для View свойство типа CaptureMouse.
Доработать HandleMouse, ForwardCtrlMsg, HandleCtrlMsg с учетом того,
что, если CaptureMouse = TRUE, то Controllers.PollCursorMsg тоже обрабатывается не зависимо от того где сейчас мышь. Причем
Controllers.PollCursorMsg получает только тот View над которым был произведен щелчок мыши (MouseDown)
ввести сообщение типа Controllers.MouseUpMsg
избавиться от кода типа: REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; в модулях Blackbox и все будет работать корректно.
Лучше все-таки перевести Blackbox на режим:

нажата_мышь: Захват мыши, если CaptureMouse = TRUE
мыши_движется: Обработка движения во View
мышь_отпущена: ReleaseMouse

Простота обработок типа REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; только кажущаяся, проблем от этого наоборот еще больше.

Автор:  Александр Ильин [ Понедельник, 13 Август, 2007 19:49 ]
Заголовок сообщения:  Re:

Штирлиц писал(а):
Далее можно ввести для View свойство типа CaptureMouse.
Доработать HandleMouse, ForwardCtrlMsg, HandleCtrlMsg с учетом того,
что, если CaptureMouse = TRUE, то Controllers.PollCursorMsg тоже обрабатывается не зависимо от того где сейчас мышь. Причем
Controllers.PollCursorMsg получает только тот View над которым был произведен щелчок мыши (MouseDown)
ввести сообщение типа Controllers.MouseUpMsg
избавиться от кода типа: REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; в модулях Blackbox и все будет работать корректно.
Лучше все-таки перевести Blackbox на режим:

нажата_мышь: Захват мыши, если CaptureMouse = TRUE
мыши_движется: Обработка движения во View
мышь_отпущена: ReleaseMouse

Простота обработок типа REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; только кажущаяся, проблем от этого наоборот еще больше.

Ну как, попробовали реализовать? Работает? Какие типичные проблемы встретились? Действительно ли простота кажущаяся?

Автор:  Штирлиц [ Понедельник, 13 Август, 2007 20:44 ]
Заголовок сообщения:  Re: Проблема Ports.Rider.Input

Александр Ильин писал(а):
Штирлиц писал(а):
Далее можно ввести для View свойство типа CaptureMouse.
Доработать HandleMouse, ForwardCtrlMsg, HandleCtrlMsg с учетом того,
что, если CaptureMouse = TRUE, то Controllers.PollCursorMsg тоже обрабатывается не зависимо от того где сейчас мышь. Причем
Controllers.PollCursorMsg получает только тот View над которым был произведен щелчок мыши (MouseDown)
ввести сообщение типа Controllers.MouseUpMsg
избавиться от кода типа: REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; в модулях Blackbox и все будет работать корректно.
Лучше все-таки перевести Blackbox на режим:

нажата_мышь: Захват мыши, если CaptureMouse = TRUE
мыши_движется: Обработка движения во View
мышь_отпущена: ReleaseMouse

Простота обработок типа REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown; только кажущаяся, проблем от этого наоборот еще больше.

Ну как, попробовали реализовать? Работает? Какие типичные проблемы встретились? Действительно ли простота кажущаяся?


Нет не пробовал.
Просто я убедился на примере ObxView5:
переписав немного вот так (еще до того как вышел патч устраняющий 100% загрузку процессора)

PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
VAR msg: Controllers.Message;
VAR focus: Views.View);
VAR x, y, w, h: INTEGER; m: SET; isDown: BOOLEAN;
BEGIN
WITH msg: Controllers.PollOpsMsg DO
msg.valid := {Controllers.paste}; msg.selectable := TRUE;
msg.type := "Obx.Tutorial"
| msg: Controllers.EditMsg DO
IF msg.op = Controllers.pasteChar THEN (* cursor keys *)
IF msg.char = 1DX THEN INC(v.x, Ports.mm)
ELSIF msg.char = 1CX THEN DEC(v.x, Ports.mm)
ELSIF msg.char = 1EX THEN DEC(v.y, Ports.mm)
ELSIF msg.char = 1FX THEN INC(v.y, Ports.mm)
END;
Views.Update(v, Views.keepFrames)
END
| msg: Controllers.TrackMsg DO
v.x := msg.x; v.y := msg.y;
v.context.GetSize(w, h); v.Restore(f, 0, 0, w, h);
f.Input(x, y, m, isDown);
doUpdate := isDown;
| msg: Controllers.PollCursorMsg DO
msg.cursor := Ports.graphicsCursor;
f.Input(x, y, m, isDown);

IF isDown THEN
IF (x # v.x) OR (y # v.y) THEN
v.context.GetSize(w, h);
v.x := x; v.y := y; v.Restore(f, 0, 0, w, h);
END;
END;
IF ~isDown & doUpdate THEN
doUpdate := FALSE;
Views.Update(v, Views.keepFrames);
END;
ELSE (* ignore other messages *)
END
END HandleCtrlMsg;

Что схема должна работать, что процессор не грузится.
Ввести надо еще дополнительное сообщение типа Controllers.MouseUpMsg а дальше понял, что надо ... :) подсистему Host переписать и
много чего еще, чтобы избавиться по всему коду от REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown;

Страница 1 из 1 Часовой пояс: UTC + 3 часа
Powered by phpBB® Forum Software © phpBB Group
https://www.phpbb.com/