Поддержка MultiTouch + Gestures в XE4
Как-то незаметно для меня прошли все эти новые веяния в виде активных мониторов, на которые пользователь может тыкать пальцами. И знать бы о них не знал, кабы месяца три назад начальство не приобрело себе ноутбук, который можно порвать на две части (экран отдельно, клавиатура отдельно), причем не Surface какой-то, пропиаренный MS, а уже самый проходной у пользователей — от ASUS, за гораздо меньшие деньги (относительно).Да и закуплен был данный девайс не просто так — задача выросла оттуда, откуда и не ждали.
Де юре: мы проводим огромное количество семинаров в месяц, и вот именно на них, нашим лекторам начали демонстрировать несовместимость нашего ПО с этим пресловутым тачем.
Де факто: на почту саппорта начали сыпаться гневные письма пользователей плана — «я два раза тапнула, а оно не тапнулось, может не так тапнула? «А начальство все это скрупулезно отслеживало на своем «порватом» ноутбуке и готовило ТЗ.
И вот настал тот день. Мне на рабочий стол был воздвигнут третий монитор, 23 дюйма от LG (с поддержкой Touch ввода аж 10 пальцами) и поставлена задача — это должно заработать в течении трех дней!
А работаю то я в XE4 — беда.
0. Анализ проблемы Благо я знаком с множеством компетентных товарищей (включая Embarcadero MVP), с которыми можно посоветоваться, с какой стороны вообще подойти к поддержке Touch, но… вычитав досконально линки на технические статьи (присланные ими) о поддержке мультитача, я понял, что в XE4 мне ничего не светит. Доступные мне возможности VCL сильно ограничены.Немного почитав конференции Embarcadero я узнал, что мультитач, с некоторыми ограничениями, стал доступен только в XE7 (однако).
Не уверен что начальство оценило бы, если бы я сказал, что самый простой способ решения задачи выглядит в виде апдейта на ХЕ7 (плюс время затраты на проверку кода на совместимость после апа).
Поэтому смотрим что мне доступно в XE4: плюсы: — она знает о жестах (Gesture).минусы: — она не знает о Touch (знает, но не предоставляет внешнего обработчика); — она не знает о Gesture при помощи двух точек ввода (двумя и более пальцами).
А теперь посмотрим что мне не доступно:
Я не могу расширить класс TRealTimeStylus введением поддержки интерфейса IRealTimeStylus3 до кучи к IStylusAsyncPlugin просто потому, что он спрятан от меня внутри TPlatformGestureEngine аж в strict private type секции класса. Мне не предоставлен полноценный обработчик сообщения WM_TOUCH, хотя данное сообщение обрабатывается внутри TWinControl.WndProc: WM_TOUCH: with FTouchManager do if (GestureEngine <> nil) and (efTouchEvents in GestureEngine.Flags) then GestureEngine.Notification (Message); Как можно увидеть по коду, управление идет сразу на движок распознавания жестов.Хотя казалось бы –, а зачем мне гестуры, если я хочу двигать пять картинок по канвасу в том порядке, который гестуры явно не распознают?
Конечно, во втором случае я могу сам перекрыть WM_TOUCH, но раз уж кто-то взялся за его обработку и получил данные, почему бы их не отдать наружу, избавив разработчика от повторного дубляжа кода?
Поэтому зайдем с другого бока.
1. Постановка задачи Наше ПО — это по сути очень сильно навороченный Excel, правда, с заточкой под определенный контингент пользователей, в данном случае сметчиков. Впрочем, немного перефразирую: дистанция между возможностями нашего софта и Excel примерно аналогична разнице между MsPaint и Adobe Photoshop.Наши пользователи тоже могут реализовать в Excel некий документ в виде сметы, так же как и рисунок в MsPaint. Весь цимус в результате.Разработан проект был по иидеологииWYSIWYG, и представляет из себя в 90 процентов случаев некий кастомный класс (от TCustomControl), реализующий грид, в котором пользователь работает, так же как с обычным бумажным документом.
Выглядит примерно так: (скриншот сделан во время операции DragDrop позиции, на стрелку не обращайте внимания, бо картинка выдрана из какой-то техсопроводиловки и указывает на плавающий Hint, типа фишка.
В этом контроле отсутствуют такие стандартные понятия, как скролл. Он, конечно, есть, но им эмулируется манипуляция с колонками в случае подвижки по горизонтали, либо в случае вертикального смещения — переход на следующую строку листа.Он не воспринимает стандартные сообщения о скролировании.
В базовом варианте (который выдает ОС) он умеет получать события о клике мышки, эмулируемое системой через тап на тачскрине, и WM_MOUSEMOVE, так же эмулируемое системой через тач.
А что нужно:
Единственное, что умеет текущий вариант Gesture — тап двумя пальцами для вызова PopupMenu по координатам тапа; Скролирование влево/право/вверх/вниз посредством свайпа двумя пальцами на тачскрине; Эмуляция команд «назад/вперед», посредством свайпа тремя пальцами на тачскрине. С учетом того что Gesture в XE4 принципиально не заточены на мультитач (даже на уровне редактора гестур), а задачу решать надо, я грустил целый вечер и… с утра приступил к работе.2. Используемые термины Как я говорил ранее, я не огромный специалист во всех этих новых веяниях, поэтому в статье я буду оперировать следующими определениями (вполне вероятно, что неправильными): Тап — аналог клика мышкой, событие, возникающее при одинарном коротком нажатии пальцем на тачскрин.Тач (или точка тача) — нечто описывающее ситуацию, когда палец контактирует с тачскрином (и обрабатывается сообщение WM_TOUCH).Маршрут — список координат, над которыми пользователь провел пальцем (перемещалась точка тача).Сессия — начинается, когда палец коснулся тачскрина, продолжается, когда пользователь водит по нему, не отпуская пальца, и завершается, когда палец убран. На протяжении сессии строится ее маршрут.Жест (Gesture) — некий шаблонный эталон маршрута, с которым сравнивается маршрут сессии. К примеру пользователь ткнул пальцем, потянул влево и отпустил — это жест с идентификатором sgiLeft.
3. Разбираемся с обработкой WM_TOUCH Для начала необходимо определиться –, а поддерживает ли вообще наше железо мультитач? Для этого достаточно вызвать GetSystemMetrics с параметром SM_DIGITIZER и проверить результат на наличие двух флагов: NID_READY и NID_MULTI_INPUT.Грубо:
tData:= GetSystemMetrics (SM_DIGITIZER); if tData and NID_READY <> 0 then if tData and NID_MULTI_INPUT <> 0 then … все хорошо, можно работать К сожалению, если у вас нет устройств с поддержкой мультитача работающих на OS Windows, то дальнейшая часть статьи будет для вас просто теорией, без возможности проверки результата. Можно попробовать использовать эмулятор тача из Microsoft® Surface® 2.0 SDK, но я с ним не экспериментировал.НО!!! Если ваш девайс поддерживает мультитач, то можно попробовать его пощупать. Для этого выберем произвольное окно (к примеру главную форму) и скажем:
RegisterTouchWindow (Handle, 0); Без вызова данной функции наше выбранное окно не будет принимать сообщения WM_TOUCH.«Отлучить» окно от получения данного сообщения поможет функция UnregisterTouchWindow.Декларируем обработчик сообщения WM_TOUCH.
procedure WmTouch (var Msg: TMessage); message WM_TOUCH; И начинаем разбираться — что он нам вообще дает.Итак, параметр WParam данного сообщения содержит количество активных точек тача, о котором нам хочет сообщить система. Причем это число хранится только в нижних двух байтах, что намекает о возможности поддержки системой до 65535 точек ввода.
Я пытался такое прикинуть — не получилось, бо у меня монитор держит максимум 10 пальцев. Хотя, в этом и есть цимус, если оглядываться на современные фантастические фильмы, где показаны некие виртуальные столы с данными, с которыми работает куча людей, имеющих возможность туда тыкать всеми десятью каждый (ну, к примеру, «Аватар» тот-же, или «Обливион»).
Молодцы, заложились на перспективу, хотя, как оказалось — это уже давно работает и без фильмов, просто я не всегда слежу за новинками. К примеру, вот такой 46 дюймовый девайс был представлен на выставке «Consumer Electronics Show 2011»:
Впрочем, не будем отвлекаться: А вот LParam данного сообщения является неким хэндлом, через который можно получить более подробную информацию о сообщении посредством вызова функции GetTouchInputInfo.Если после вызова GetTouchInputInfo повторный вызов данной функции не требуется, то MSDN рекомендует сказать CloseTouchInputHandle, но это не обязательно, т.к. очистка данных в куче все равно произойдет автоматом при передаче управления на DefWindowProc или при попытке отправки данных через SendMessage/PostMessage.Более подробнее тут.
Что от нас требует функция GetTouchInputInfo:
Ей необходим сам хэндл, с которым она будет работать; Ей необходим выделенный буфер ввиде массива из элементов TTouchInput, в котором она разместит всю информацию о событии; Размер этого массива; Размер каждого элемента массива. Опять молодцы: при помощи четвертого пункта, сразу заложились на возможность изменения структуры TTouchInput в следующих версиях ОС (даже интересно, что туда еще можно добавить? Если сильно грубо, то ее вызов выглядит так:
var Count: Integer; Inputs: array of TTouchInput; begin Count:= Msg.WParam and $FFFF; SetLength (Inputs, Count); if GetTouchInputInfo (Msg.LParam, Count, @Inputs[0], SizeOf (TTouchInput)) then // … что-то делаем с полученной информацией CloseTouchInputHandle (Msg.LParam); Это все. А теперь попробуем разобраться с данными, которые хранятся в массиве Inputs.4. Обрабатываем TTouchInput С этого самого момента начинается самое интересное.Размер массива TTouchInput зависит от того, сколько пальцев приложено к тачскрину.Для каждой точки тача (пальца) система генерирует уникальный ID, который не изменяется в течении всей сессии (от момента касания пальцем, до… пока мы его не убрали).Этот ID отображен на каждый элемент TTouchInput массива и хранится в параметре dwID.
Кстати о сессиях: Сессия, это… Ну давайте вот так:
На картинке отображено ровно 10 сессий (под каждый палец), показан их маршрут (массив точек над которыми перемещался палец в рамках каждой сессии), причем, каждая из сессий еще не завершена (пальцы все еще приложены к тачскрину).Впрочем, вернемся обратно к структуре TTouchInput.По сути, для нормальной работы с тачем от данной структуры нам требуется всего лишь несколько параметров:
TOUCHINPUT = record x: Integer; // абсолютные координаты y: Integer; // точки тача hSource: THandle; // хэндл окна, обрабатывающего сообщение dwID: DWORD; // уникальный идентификатор точки dwFlags: DWORD; // текущее состояние точки // все остальное в принципе не нужно dwMask: DWORD; dwTime: DWORD; dwExtraInfo: ULONG_PTR; cxContact: DWORD; cyContact: DWORD; end; Давайте сразу начнем с реализации демо-приложения.Создайте новый проект и на главной форме разместите TMemo, в которое быдет выводится лог работы с тачем.В конструкторе формы подключаем ее к обработке сообщения WM_TOUCH:
procedure TdlgSimpleTouchDemo.FormCreate (Sender: TObject); begin RegisterTouchWindow (Handle, 0); end; Теперь пишем обработчик события: procedure TdlgSimpleTouchDemo.WmTouch (var Msg: TMessage); function FlagToStr (Value: DWORD): string; begin Result:= ''; if Value and TOUCHEVENTF_MOVE <> 0 then Result:= Result + 'move '; if Value and TOUCHEVENTF_DOWN <> 0 then Result:= Result + 'down '; if Value and TOUCHEVENTF_UP <> 0 then Result:= Result + 'up '; if Value and TOUCHEVENTF_INRANGE <> 0 then Result:= Result + 'ingange '; if Value and TOUCHEVENTF_PRIMARY <> 0 then Result:= Result + 'primary '; if Value and TOUCHEVENTF_NOCOALESCE <> 0 then Result:= Result + 'nocoalesce '; if Value and TOUCHEVENTF_PEN <> 0 then Result:= Result + 'pen '; if Value and TOUCHEVENTF_PALM <> 0 then Result:= Result + 'palm '; Result:= Trim (Result); end; var InputsCount, I: Integer; Inputs: array of TTouchInput; begin // получаем количество точек тача InputsCount:= Msg.WParam and $FFFF; // подготавливаем соответствующий массив данных SetLength (Inputs, InputsCount); // получаем информацию по текущему событию if GetTouchInputInfo (Msg.LParam, InputsCount, @Inputs[0], SizeOf (TTouchInput)) then begin // закрываем хэндл (а можно и не закрывать) CloseTouchInputHandle (Msg.LParam); // выводим информацию на экран for I:= 0 to InputsCount — 1 do Memo1.Lines.Add (Format ('TouchInput №: %d, ID: %d, flags: %s', [I, Inputs[I].dwID, FlagToStr (Inputs[I].dwFlags)])); end; end; Это все.Согласитесь — просто до невозможности. Все данные перед глазами.Попробуйте поэкспериментировать с этим кодом с использованием тачскрина и вы заметите, что разработчику, помимо привязки к ID каждого тача, передается еще определенный набор флагов, которые выводятся в лог.По данным лога сразу можно определить начало сессии тача (флаг TOUCHEVENTF_DOWN), перемещение каждого из пальцев по тачскрину (флаг TOUCHEVENTF_MOVE) и завершение сессии (флаг TOUCHEVENTF_UP).
Выглядит вот так:
[embedded content]
Сразу оговорюсь об одной неприятности: не всегда в обработчик WM_TOUCH будут приходить сообщения от тачскрина с флагами TOUCHEVENTF_DOWN или TOUCHEVENTF_UP. Этот нюанс нужно учитывать при реализации своих «классов-оберток», о которых пойдет речь чуть ниже.
К примеру: Наше приложение в данный момент отображает PopupMenu — нажатие на тачскрин приведет к его закрытию, но сообщение WM_TOUCH с флагом TOUCHEVENTF_DOWN к нам не придет, хотя последующие, с флагом TOUCHEVENTF_MOVE, мы получим достаточно успешно.То же относится и к показу PopupMenu в обработчике события TOUCHEVENTF_MOVE.В данном случае произойдет срыв сессии и сообщения WM_TOUCH с флагом TOUCHEVENTF_UP ждать не стоит.
Такое поведение наблюдается под Windows 7 (32/64 бита), я даже допускаю — под Windows 8 и выше что-то поменялось, но у меня просто нет возможности проверить это сейчас (лень — второе я).
Впрочем, получив представление о том «как это работает», попробуем написать нечто более интересное.
Исходный код примера в папке ».\demos\simple\» в архиве с исходниками.
5. Применяем мультач на практике. Мой монитор держит 10 пальцев одновременно, можно даже написать приложение, эмулирующее рояль (правда в рояле есть еще педали и чувствительность к силе нажатия), но зачем идти сразу от сложного? Самое простое, что пришло мне в голову — это 10 квадратов на канвасе формы, которые я могу двигать во все стороны посредством тача.Этого вполне достаточно, чтобы «пощупать» мультитач в самом прямом смысле.Создаем новый проект.
Каждый из квадратов будет описываться в виде такой структуры:
type TData = record Color: TColor; ARect, StartRect: TRect; StartPoint: TPoint; Touched: Boolean; TouchID: Integer; end; По сути, самым важным полем данной структуры является TouchID, все остальное второстепенно.Нам нужно где-то хранить данные по каждому квадрату, поэтому объявим их в виде такого массива:
FData: array [0…9] of TData; Ну, и выполним инициализацию: procedure TdlgMultiTouchDemo.FormCreate (Sender: TObject); var I: Integer; begin DoubleBuffered:= True; RegisterTouchWindow (Handle, 0); Randomize; for I:= 0 to 9 do begin FData[I].Color:= Random ($FFFFFF); FData[I].ARect.Left:= Random (ClientWidth — 100); FData[I].ARect.Top:= Random (ClientHeight — 100); FData[I].ARect.Right:= FData[I].ARect.Left + 100; FData[I].ARect.Bottom:= FData[I].ARect.Top + 100; end; end; А так же их отрисовку на канвасе формы (пока что не анализируйте обработчик FormPaint, мы дойдем до него чуть ниже): procedure TdlgMultiTouchDemo.FormPaint (Sender: TObject); var I: Integer; begin Canvas.Brush.Color:= Color; Canvas.FillRect (ClientRect); for I:= 0 to 9 do begin Canvas.Pen.Color:= FData[I].Color xor $FFFFFF; if FData[I].Touched then Canvas.Pen.Width:= 4 else Canvas.Pen.Width:= 1; Canvas.Brush.Color:= FData[I].Color; Canvas.Rectangle (FData[I].ARect); end; end; Запустите, получится как-то так: Обвес готов, теперь попробуем изменить картинку через обработку WM_TOUCH.
Все что нам нужно в обработчике, это получить индекс квадрата, над которым пользователь нажал пальцем. Но для начала переведем координаты от каждой точки тача в координаты окна:
pt.X:= TOUCH_COORD_TO_PIXEL (Inputs[I].x); pt.Y:= TOUCH_COORD_TO_PIXEL (Inputs[I].y); pt:= ScreenToClient (pt); Имея на руках валидные координаты, мы можем узнать индекс квадрата в массиве, посредством вызова PtInRect. function GetIndexAtPoint (pt: TPoint): Integer; var I: Integer; begin Result:= -1; for I:= 0 to 9 do if PtInRect (FData[I].ARect, pt) then begin Result:= I; Break; end; end; Когда пользователь только коснулся пальцем тачскрина (учитывая что каждая точка, обладает своим уникальным ID), мы присвоим найденному квадрату данный ID. Пригодится в дальнейшем: if Inputs[I].dwFlags and TOUCHEVENTF_DOWN <> 0 then begin Index:= GetIndexAtPoint (pt); if Index < 0 then Continue; FData[Index].Touched := True; FData[Index].TouchID := Inputs[I].dwID; FData[Index].StartRect := FData[Index].ARect; FData[Index].StartPoint := pt; Continue; end; Это, скажем так, инициализация объекта и начало сессии тача.Следующее сообщение, которое мы получим, скорее всего будет WM_TOUCH с флагом TOUCHEVENTF_MOVE.
Тут нюанс: В первом случае мы искали квадраты по их координатам, а сейчас это будет ошибкой, хотя бы потому, что позиции квадратов на форме могут пересекаться.Поэтому, в случае MOVE, мы будем искать квадраты по ID тача, который был выставлен через параметр TouchID:
function GetIndexFromID (ID: Integer): Integer; var I: Integer; begin Result:= -1; for I:= 0 to 9 do if FData[I].TouchID = ID then begin Result:= I; Break; end; end; Найдя необходимый нам квадрат, делаем подвижку, ориентируясь на структуру заданную в начале тач сессии: R:= FData[Index].StartRect; OffsetRect (R, pt.X — FData[Index].StartPoint.X, pt.Y — FData[Index].StartPoint.Y); FData[Index].ARect:= R; Ну, и концовка в виде обработки флага TOUCHEVENTF_UP:
if Inputs[I].dwFlags and TOUCHEVENTF_UP <> 0 then begin FData[Index].Touched:= False; FData[Index].TouchID:= -1; Continue; end; В которой мы отключаем квадрат от тач сессии и перерисовываем сам канвас.Крайне простой примерчик, который, однако, работает и денег не просит.Запускайте и тестируйте — получается достаточно забавно:
[embedded content]
Просто для «красявости», параметр Touched структуры TData, используется внутри FormPaint и отвечает за присутствие «жирной» рамки вокруг перемещаемого квадрата.
Исходный код примера в папке ».\demos\multutouch\» в архиве с исходниками.
6. Разбираемся с Gesture (жестами) Мультитач — это только первый шажок, ибо нам хотелось бы работать с мультач-жестами, но…Давайте, для начала, посмотрим как реализовано в VCL распознавание жеста на основе одной Touch сессии (одним пальцем).За это отвечает класс TGestureEngine от которого потребуется, в принципе, только код функции IsGesture ().
Рассмотрим ее поподробнее:
Она разделена ровно на две части, где первая часть проверяет стандартные жесты в цикле:
// Process standard gestures if gtStandard in GestureTypes then А вторая — некие кастомные жесты, переданные пользователем: // Process custom gestures if CustomGestureTypes * GestureTypes = CustomGestureTypes then Так как кастомные пользовательские жесты нам по определению не нужны, рассмотрим только первую часть функции.Основная ее идея выглядит в виде поиска описателя жеста через вызов FindStandardGesture и сравнением его с переданным маршрутом посредством Recognizer.Match.Все остальные параметры, приходящие в IsGesture, по сути, можно исключить — они являются обвесом функции.
Фишка в том, что Recognizer — это не интерфейс IGestureRecognizer, а VCL обертка.Вот она-то нам и нужна.
Но прежде чем перейти к написанию демопримера, нужно разобраться с тем, что из себя представляет сам жест (Gerture):
Это структура вида:
TStandardGestureData = record Points: TGesturePointArray; GestureID: TGestureID; Options: TGestureOptions; Deviation: Integer; ErrorMargin: Integer; end; Points — это маршрут жеста, с которым сравнивается аналогичный маршрут из touch сессии пользователя.GestureID — уникальный идентификатор жеста.В ХЕ4 они перечислены в модуле Vcl.Controls: const // Standard gesture id’s sgiNoGesture = 0; sgiLeft = 1; sgiRight = 2; … Options — в данном случае они нам не интересны.Deviation и ErrorMargin — параметры, указывающие величину, скажем так: «тремора» пальца в процессе жеста. Вряд ли вы сможете провести идеально ровную линию по оси Х влево без изменения позиции по оси Y, поэтому Deviation и ErrorMargin указывают на границы, в рамках которых перемещения точки будут валидны.
Декларации параметров стандартных жестов можно найти в модуле Vcl.Touch.Gestures: { Standard gesture definitions } const PDefaultLeft: array[0…1] of TPoint = ((X:200; Y:0), (X:0; Y:0)); CDefaultLeft: TStandardGestureData = ( GestureID: sgiLeft; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); PDefaultRight: array[0…1] of TPoint = ((X:0; Y:0), (X:200; Y:0)); CDefaultRight: TStandardGestureData = ( GestureID: sgiRight; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); PDefaultUp: array[0…1] of TPoint = ((X:0; Y:200), (X:0; Y:0)); CDefaultUp: TStandardGestureData = ( GestureID: sgiUp; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); … Таким образом, зная о формате жестов, мы можем самостоятельно в рантайме подготовить собственный вариант жеста, заполнив его маршрут (Points) и выставив уникальный ID.Впрочем, сейчас нам это не понадобится. Посмотрим, что можно сделать на основе стандартных жестов.Пишем самый простой пример, при помощи которого Recognizer возвратит нам ID опознанного им жеста, в котором мы построим 4 массива точек, которые технически будут похожи на те маршруты, которые пользователь будет вводить посредством тачскрина.
Например вот такой: program recognizer_demo; {$APPTYPE CONSOLE} {$R *.res} uses Windows, Vcl.Controls, SysUtils, TypInfo, Vcl.Touch.Gestures; type TPointArray = array of TPoint; function GetGestureID (Value: TPointArray): Byte; var Recognizer: TGestureRecognizer; GestureID: Integer; Data: TStandardGestureData; Weight, TempWeight: Single; begin Weight:= 0; Result:= sgiNone; Recognizer:= TGestureRecognizer.Create; try for GestureID:= sgiLeft to sgiDown do begin FindStandardGesture (GestureID, Data); TempWeight:= Recognizer.Match (Value, Data.Points, Data.Options, GestureID, Data.Deviation, Data.ErrorMargin); if TempWeight > Weight then begin Weight:= TempWeight; Result:= GestureID; end; end; finally Recognizer.Free; end; end; const gesture_id: array [sgiNone…sgiDown] of string = ( 'sgiNone', 'sgiLeft', 'sgiRight', 'sgiUp', 'sgiDown' ); var I: Integer; Data: TPointArray; begin SetLength (Data, 11); // якобы делаем жест вправо for I:= 0 to 10 do begin Data[I].X:= I * 10; Data[I].Y:= 0; end; Writeln (gesture_id[GetGestureID (Data)]); // якобы делаем жест влево for I:= 0 to 10 do begin Data[I].X:= 500 — I * 10; Data[I].Y:= 0; end; Writeln (gesture_id[GetGestureID (Data)]); // якобы делаем жест вверх for I:= 0 to 10 do begin Data[I].X:= 0; Data[I].Y:= 500 — I * 10; end; Writeln (gesture_id[GetGestureID (Data)]); // якобы делаем жест вниз for I:= 0 to 10 do begin Data[I].X:= 0; Data[I].Y:= I * 10; end; Writeln (gesture_id[GetGestureID (Data)]); Readln; end. После запуска должны увидеть следующую картинку: Что и предполагалось.Исходный код примера в папке ».\demos\recognizer\» в архиве с исходниками.
А теперь…
7. Распознаем мультитач жесты (Gestures) Данная глава описывает основную идею данной статьи, скажем так — фишку, ради которой и появился весь это текст.Сейчас — никаких технических деталей, только сам подход: Итак, что нам сейчас доступно:
Мы знаем как снимать данные с каждой тач-сессии; Мы можем распознавать жест каждой тач-сессии. К примеру: Пользователь нажал пальцем на тачскрин и провел влево; Мы зафиксировали начало сессии в обработчике ON_TOUCH + TOUCHEVENTF_DOWN, записали все точки маршрута по приходу TOUCHEVENTF_MOVE и в тот момент, когда нам пришел TOUCHEVENTF_UP, передали ранее записанный массив точек функции GetGestureID; Вывели результат. Но представьте, что пользователь сделал все то же самое, только двумя пальцами одновременно: Для каждого пальца мы стартуем собственную сессию; Пишем ее маршрут; По завершении каждой сессии передаем ее на распознание жеста. Если ID жестов от двух сессий, произведенных над одним и тем-же окном, совпадут (к примеру, это будет sgiLeft), то мы можем сделать вывод — произошел свайп влево двумя пальцами.А что если все точки маршрута сессии содержат одни и те же координаты? Тогда жеста не было и произошел так называемый тап (одним или многими пальцами).Причем под данное условие попадет также жест «Press And Tap», при помощи которого обычно отображают PopupMenu.
Таким образом, с учетом основной постановки задачи, мы можем контролировать все требуемые нам варианты жестов одним, двумя и тремя пальцами (впрочем, хоть всеми десятью).
А что делать, если жесты от двух сессий не совпали? Анализировать их, и хотя в текущую постановку задачи это не входит, можно с уверенностью сказать, что жест sgiLeft от первой сессии плюс жест sgiRight от второй, может трактоваться как Zoom. Даже Rotate вполне возможно задетектировать на основе жестов sgiSemiCircleLeft или sgiSemiCircleRight только на основе двух тач сессий.
Прониклись?
Вот дефолтовый список жестов, которые таким образом легко можно эмулировать: Windows Touch Gestures Overview
К сожалению, почему-то все это не реализовано в ХЕ4 и стало доступно только начиная с седьмой версии (и то не уверен что полностью).
8. Техническое планирование движка С теоретической частью закончили, теперь пришла пора все это применить на практике и сразу рассмотреть несколько проблем, встающих перед разработчиком.Проблема номер раз: В приложении обычно сотни окон — большинству из них достаточно того, что система генерирует при таче сообщения плана WM_LBUTTONCLICK и прочие, которых для нормального поведения окна достаточно (к примеру для кнопок, эдитов, скролов), но вот для того же SysListView32 скролирование, посредством жеста двумя пальцами, не происходит, ввиду отсутствия генерации сообщения WM_SCROLL. А ведь есть еще и кастомные контролы.Расширять оконную процедуру каждого окна — слишком много работы, поэтому нужно как-то определиться — какие окна должны поддерживать мультитач, причем сделать это необходимо наиболее универсально.Отсюда следует: нужен некий менеджер мультитача, в котором окна будут регистрироваться и который будет отвечать за всю работу с мультитачем.
Проблема номер два: Раз мы пишем нечно универсальное, не переписывая каждый экземпляр TWinControl, то необходимо как-то отслеживать пересоздание окна, благо вызовы RecreateWnd один из штатных механизмом VCL. Если мы не будем этого делать, то при первом же пересоздании окна, ранее зарегистрированный нами TWinControl, перестанет получать сообщения WM_TOUCH и, таким образом, вся работа нашего менеджера будет нивелирована.
Проблема номер три: Менеджер должен хранить все данные о тач-сессиях и уметь обрабатывать ситуации срыва начала и конца сессий (ибо не всегда приходят уведомления c флагами Down и Up), причем необходимо учитывать, что длина сессии может быть продолжительна по времени, что влечет за собой достаточно большой расход памяти, если сохранять все точки маршрута сессии.
Еще хотелось бы чтобы менеджер мультитача мог различать жесты в рамках разных окон.К примеру — если пользователь поставил два пальца в левое окно и два пальца в правое (четыре мультитач сессии), после чего соединил пальцы в центре, левому окну должно прийти уведомление о двупальцевом жесте вправо, а правому о двупальцевом жесте влево.
Но, к сожалению, это не получится, т.к. сообщение WM_TOUCH будет приходить только тому окну, в котором началась сессия, остальные окна будут игнорироваться.
9. Строим базовый каркас мультитач движка Для начала определимся с нюансами реализации класса.Технически, самое удобное, с точки зрения внешнего программиста, будет реализация некоего универсального движка, который возьмет на себя всю работу и будет уведомлять разработчика разве что вызовом финальных событий.В таком случае, разработчику придется только единожды зарегистрировать нужное окно в движке и анализировать поступающие от него жесты (направленные конкретному окну), обрабатывая нужные. К примеру, эмулируя тот же скролл двупальцевым жестом.
Сам движок будет реализован ввиде синглтона.Во первых: нет смысла плодить инстансы класса, которые всегда будут делать одно и тоже. Это не TStringList, заточенный под хранение данных, а все таки движок, реализующий единую логику работы для всех окон проекта.А во вторых: есть небольшой нюанс в реализации самого движка (о нем чуть позже), из-за которого реализация в виде синглтона будет самой простой, иначе придется кардинально переусложнять логику работы класса.
Таким образом, движок должен предоставлять:
Методы регистрации окна и снятия окна с регистрации: Набор внешних событий, обработчики которых должен реализовать разработчик. Внешние события могут быть примерно такими: OnBeginTouch — это событие будет вызываться при получении сообщения WM_TOUCH.
Поясню: в четвертой главе был приведен следующий код:
// получаем количество точек тача InputsCount:= Msg.WParam and $FFFF; Т.е. реальных точек тача может быть несколько.Вот об их количестве мы и предупредим разработчика.OnTouch — в этом событии мы уведомим разработчика о данных, содержащихся в каждой структуре TTouchInput, только немного в более причесанном виде. (Переведем данные о точке в координаты окна, выставим правильные флаги и прочее, зачем нагружать разработчика избыточной информацией и заставлять его писать избыточный код?)
OnEndTouch — этим мы скажем что цикл обработки сообщения WM_TOUCH завершен, можно, к примеру, вызвать Repaint.
OnGecture —, а это сообщение разработчик получит тогда, когда движок примет решение что жест распознан.
Так как класс реализован в виде синглтона, а зарегистрированных в нем окон будет более чем одно, то объявить все четыре события в виде свойств класса не получится.
Ну точнее как: можно конечно, но второе зарегистрированное окно сразу переназначит обработчики событий на себя и первому придется тихо курить в сторонке.Поэтому, помимо списка зарегистрированных окон, мы должны держать и обработчики событий движка, которые за ними закреплены.
Впрочем, попробуем теперь это все реализовать на практике.Создайте новый проект и к нему добавьте новый модуль, с названием… ну, к примеру, SimpleMultiTouchEngine.
Для начала объявим флаги, которые нам интересны при обработке WM_TOUCH:
type TTouchFlag = ( tfMove, // перемещаем точку tfDown, // создали току тача tfUp // прекратили работу с точкой ); TTouchFlags = set of TTouchFlag; Опишем структуру, которую мы будем передавать на внешку разработчику о каждой точке: TTouchData = record Index: Integer; // порядковый номер точки в массиве TTouchInput ID: DWORD; // неизменяющееся ID точки Position: TPoint; // её координаты относительно окна Flags: TTouchFlags; // флаги end; Декларация события OnTouchBegin будет выглядеть так: TTouchBeginEvent = procedure (Sender: TObject; nCount: Integer) of object; А так будет выглядеть OnTouch: TTouchEvent = procedure (Sender: TObject; Control: TWinControl; TouchData: TTouchData) of object; Для OnEndTouch будет достаточно обычного TNotifyEvent.Данные о назначенных обработчиках событий, закрепленных за каждым зарегистрированным окном, будут хранится в такой структуре:
TTouchHandlers = record
BeginTouch: TTouchBeginEvent;
Touch: TTouchEvent;
EndTouch: TNotifyEvent;
end;
Декларируем новый класс:
TSimleMultiTouchEngine = class
private const
MaxFingerCount = 10;
private type
TWindowData = record
Control: TWinControl;
Handlers: TTouchHandlers;
end;
private
FWindows: TList
Структура TWindowData — содержит в себе зарегистрированное окно и список обработчиков, которые назначил программист.
Поле FWindows: TList — список зарегистрированных окон и обработчиков, от которого мы и будем плясать на всем протяжении работы с класом.
Поле FMultiTouchPresent — флаг, инициализирующийся в конструкторе класса.Содержит True, если наше железо держит мультитач. Опираясь на данный флаг будет отключаться часть логики класса (зачем делать лишние телодвижения тогда, когда мы их выполнить все равно не сможем?).
Первая protected секция — просто для удобства вынесены все вызовы внешних событий.
Процедура HandleTouch — основное ядро движка, именно она и отвечает за обработку сообщения WM_TOUCH.
Процедура HandleMessage — вспомогательная. Ее задача определить к какому из зарегистрированных окон направлено сообщение и вызвать HandleTouch, передав индекс найденного окна.
Паблик секция — конструктор, деструктор, регистрация окна и снятие его с регистрации.
Прежде чем приступить к реализации класса, сразу напишем синглтон обвес:
function MultiTouchEngine: TSimleMultiTouchEngine;
implementation
var
_MultiTouchEngine: TSimleMultiTouchEngine = nil;
function MultiTouchEngine: TSimleMultiTouchEngine;
begin
if _MultiTouchEngine = nil then
_MultiTouchEngine:= TSimleMultiTouchEngine.Create;
Result:= _MultiTouchEngine;
end;
…
initialization
finalization
_MultiTouchEngine.Free;
end.
И, в завершении всего, каллбэк ловушки, при помощи которой мы будем получать сообщения WM_TOUCH, отправленные зарегистрированным в движке окнам.
var
FHook: HHOOK = 0;
function GetMsgProc (nCode: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall;
begin
if (nCode = HC_ACTION) and (WParam = PM_REMOVE) then
if PMsg (LParam)^.message = WM_TOUCH then
MultiTouchEngine.HandleMessage (PMsg (LParam));
Result:= CallNextHookEx (FHook, nCode, WParam, LParam);
end;
На всякий случай, список используемых модулей выглядит так:
uses
Windows,
Messages,
Classes,
Controls,
Generics.Defaults,
Generics.Collections,
Vcl.Touch.Gestures;
Ну, а теперь пройдемся по реализации самого движка. Начнем, пожалуй, с конструктора.
constructor TSimleMultiTouchEngine.Create;
var
Data: Integer;
begin
// проверяем, есть ли поддержка мультитача
Data:= GetSystemMetrics (SM_DIGITIZER);
FMultiTouchPresent:=
(Data and NID_READY <> 0) and (Data and NID_MULTI_INPUT <> 0);
// если нет, то и работать не с чем
if not FMultiTouchPresent then Exit;
// создаем список в котором будем хранить зарегистрированные окна
FWindows:= TList
procedure TSimleMultiTouchEngine.RegisterWindow (Value: TWinControl; Handlers: TTouchHandlers); var WindowData: TWindowData; begin // если мультитач не поддерживается — выходим if not FMultiTouchPresent then Exit; // для того чтобы IndexOf отработал, инициализируем соответствующее поле структуры WindowData.Control:= Value; // окно можно зарегистрировать только один раз, // повторная регистрация не поддерживается if FWindows.IndexOf (WindowData) < 0 then begin // запоминаем список обработчиков WindowData.Handlers := Handlers; // подключаем окно к тачу RegisterTouchWindow(Value.Handle, 0); // добавляем структуру к общему списку окон FWindows.Add(WindowData); end; // после добавления окна запускаем ловушку if FHook = 0 then FHook := SetWindowsHookEx(WH_GETMESSAGE, @GetMsgProc, HInstance, GetCurrentThreadId); end; Все прокомментировано, впрочем единственный нюанс с вызовом IndexOf. Для того чтобы он работал не через CompareMem сравнивая две структуры между собой, а только по одному полю структуры (Control) и был реализован TComparer в конструкторе класса списка.Как можно увидеть из кода — логика проста, после добавления окна в общий список, класс стартует ловушку WH_GETMESSAGE (если она ранее не была запущена), причем работающую только в пределах текущей нити.
Отдельно остановлюсь на переменной FMultiTouchPresent.Как видно из кода, она просто выполняет роль предохранителя, который отключает всю логику работы класса в том случае, если мы не можем сделать ничего полезного.Если убрать ее, то будет небольшой «overhead» в цикле выборки сообщений каждого окна нашего приложения из-за установленной ловушки в том случае, если наше «железо» вообще не имеет понятия о тачскрине. Оно нам надо?
Снятие окна с регистрации идет по такому-же принципу, с отключением ловушки, если окон больше нет:
procedure TSimleMultiTouchEngine.UnRegisterWindow (Value: TWinControl); var Index: Integer; WindowData: TWindowData; begin // если мультитач не поддерживается — выходим if not FMultiTouchPresent then Exit; // для того чтобы IndexOf отработал, инициализируем соответствующее поле структуры WindowData.Control:= Value; // ищем окно Index:= FWindows.IndexOf (WindowData); if Index >= 0 then // если нашлось, удаляем окно из списка FWindows.Delete (Index); // если окон не осталось, то ловушка нам больше не нужна if FWindows.Count = 0 then begin // выключаем ее UnhookWindowsHookEx (FHook); FHook:= 0; end; end; Собственно вся логика движка проста: приняли окно на регистрацию, запустили ловушку, которая при получении сообщения WM_TOUCH вызывает процедуру HandleMessage, посредством обращения к синглтону кл