Всякий раз, когда я захожу в Американский музей естествознания (American Museum of Natural History) в Нью-Йорке, я всегда заглядываю в зал приматов. В нем собрано огромное количество скелетов и чучел, отражающих эволюционную панораму иерархии приматов — от животных вроде крошечной тупайи, лемуров и мартышек до шимпанзе, высших приматов и людей.
Что сразу же становится понятным из этой экспозиции, так это удивительная общность всех приматов: структура костей рук (лап), включая отстоящий (большой) палец. Та же самая структура костей, которая позволяла нашим предкам и дальним родственникам обхватывать ветки деревьев и карабкаться по ним, дает возможность нашему виду манипулировать окружающей средой и изготавливать разные вещи. Происхождение наших рук может восходить к лапам крошечных приматов, живших десятки миллионов лет назад, и тем не менее они тоже являются важным фактором в том, что отличает человека.
В таком случае разве есть что-то удивительное в том, что мы пытаемся инстинктивно указать пальцем на какой-то объект на экране компьютера или даже коснуться его?
В ответ на это человеческое желание теснее соединить свои пальцы с компьютером происходила и эволюция устройств ввода. Мышь страшно хороша для выбора и перетаскивания объектов, но безнадежна для свободного рисования или письма от руки. Перо планшетного компьютера позволяет писать, но плохо подходит для растягивания или перемещения объектов. Сенсорные экраны уже стали привычными во всяческих терминалах и информационных системах музеев, но обычно они ограничены простым указанием и нажатием.
Думаю, что технология Multi-Touch (мультисенсорная технология) представляет собой большой скачок вперед. Как и подразумевает ее название, Multi-Touch выходит далеко за рамки сенсорных экранов прошлого, позволяя распознавать движения нескольких пальцев, и она сильно отличается по типам жестов, которые можно изображать на экране. Multi-Touch — это результат эволюции прежних сенсорных устройств ввода, и в то же время она предлагает принципиально другую парадигму ввода.
Multi-Touch, вероятно, наиболее ярко проявляет себя в телепередачах новостей, где картами на огромных экранах манипулирует синоптик или политический обозреватель. Microsoft исследовала мультисенсорную технологию в нескольких областях — от компьютера Microsoft Surface размером с кофейный столик до небольших устройств вроде Zune HD; кроме того, сейчас эта технология становится фактическим стандартом на смартфонах.
Хотя Microsoft Surface способен реагировать на множество пальцев одновременно (и даже содержит внутренние камеры, отслеживающие объекты на стеклянной поверхности), большинство других мультисенсорных устройств ограничено реакцией максимум на два пальца (или две точки касания). (Я буду использовать оба термина как синонимы.) Но здесь проявляется эффект синергии: на компьютерном дисплее два пальца открывают возможности не просто в два раза больше, чем один палец.
Ограничение на две точки касания определяется возможностями мультисенсорных дисплеев, которые недавно стали доступными для настольных ПК и лэптопов, а также специальной версии лэптопа Acer Aspire 1420P, которая распространялась среди участников конференции профессиональных разработчиков Microsoft (Professional Developers Conference, PDC) в ноябре прошлого года (его еще часто называют PDC-лэптопом). Эта акция дала уникальную возможность тысячам разработчиков создавать приложения с мультисенсорной поддержкой.
Именно PDC-лэптоп я и использовал для исследования мультисенсорной поддержки в Silverlight 3.
События и классы в Silverlight
Мультисенсорная поддержка становится стандартом в различных инфраструктурах и Windows API. Такая поддержка встроена в Windows 7 и в предстоящий выпуск Windows Presentation Foundation (WPF) 4. (Компьютер Microsoft Surface также использует WPF, но включает ряд расширений для своих очень специфических возможностей.)
В этой статье я хотел бы сосредоточиться на мультисенсорной поддержке в Silverlight 3. Этот вопрос освещен очень слабо, тогда как Multi-Touch весьма адекватно поддерживается и очень полезна для изучения базовых концепций мультисенсорной технологии.
Если вы опубликуете мультисенсорное приложение Silverlight на своем веб-сайте, кто им сможет пользоваться? Для этого, конечно, понадобится мультисенсорный монитор, и, кроме того, приложение Silverlight должно выполняться в операционной системе (ОС) и браузере, которые поддерживают Multi-Touch. На данный момент такую поддержку обеспечивают только Internet Explorer 8 в Windows 7, но в будущем она наверняка появится в других ОС и браузерах.
Поддержка Silverlight 3 мультисенсорной технологии заключена в пяти классах, одном делегате, одном перечислении и единственном событии. Способа определить, выполняется ли ваша программа Silverlight на мультисенсорном устройстве и, если да, сколько точек касания поддерживает это устройство, пока нет.
Приложение Silverlight, которому нужно реагировать на мультисенсорный ввод, должно подключить обработчик к статическому событию Touch.FrameReported:
Touch.FrameReported += OnTouchFrameReported;
Вы можете подключить этот обработчик события и на компьютере, на котором нет мультисенсорного монитора, — ничего плохого не случится. Событие FrameReported является единственным открытым членом статического класса Touch. Обработчик выглядит так:
void OnTouchFrameReported(
object sender, TouchFrameEventArgs args) {
...
}
В приложении можно установить несколько обработчиков Touch.FrameReported, и все они будут сообщать обо всех событиях в любой части приложения.
В TouchFrameEventArgs есть одно открытое свойство TimeStamp, которое мне ни разу не понадобилось, и три важных открытых метода:
- TouchPoint GetPrimaryTouchPoint(UIElement relativeTo);
- TouchPointCollection GetTouchPoints(UIElement relativeTo);
- void SuspendMousePromotionUntilTouchUp().
Аргумент в GetPrimaryTouchPoint или GetTouchPoints используется исключительно для передачи информации о позиции, содержащейся в объекте TouchPoint. Вместо аргумента можно подставить null — тогда вы будете получать информацию о позиции относительно верхнего левого угла основного окна приложения Silverlight.
Multi-Touch поддерживает касание экрана несколькими пальцами, и каждый палец, касающийся экрана (вплоть до максимального значения, которое в настоящее время обычно равно двум), является точкой касания. Основная точка касания (primary touch point) относится к пальцу, который касается экрана в отсутствие других прикосновений и при условии, что кнопка мыши не нажата.
Коснитесь экрана пальцем. Это основная точка касания. Не отрывая первый палец, коснитесь экрана вторым пальцем. Очевидно, что второй палец не является основной точкой касания. А теперь, удерживая второй палец на экране, поднимите первый палец и вновь коснитесь им экрана. Является ли он основной точкой касания? Нет. Основная точка касания создается, только когда экрана не касаются другие пальцы.
Основная точка касания преобразуется в точку касания, которая будет передана мыши. В реальных мультисенсорных приложениях вы не должны полагаться на основную точку касания, так как пользователи обычно не придают особого значения первому касанию.
События генерируются только в том случае, когда пальцы действительно касаются экрана. Никакого висения пальцев над экраном, даже очень близкого, не распознается.
По умолчанию действия, включающие основную точку касания, преобразуются в различные события мыши. Это позволяет существующим приложениям реагировать на касание без специфического кодирования. При простом касании экрана возникает событие MouseLeftButtonDown, при перемещении пальца, который все еще касается экрана, — MouseMove, а при поднятии пальца — MouseLeftButtonUp.
Объект MouseEventArgs, передаваемый в сообщениях от мыши, включает свойство DeviceType, которое помогает различать события мыши от событий пера и касания. Как показывает мой опыт работы с PDC-лэптопом, свойство DeviceType содержит TabletDeviceType.Mouse, если событие пришло от мыши, или TabletDeviceType.Touch, если вы коснулись экрана пальцем или пером.
В события мыши трансформируется только основная точка касания, и, как предполагает имя третьего метода объекта TouchFrameEventArgs, вы можете запретить такое преобразование. Подробнее об этом чуть позже.
Конкретное событие Touch.FrameReported может быть сгенерировано на основе одной или нескольких точек касаний. TouchPointCollection, возвращаемый методом GetTouchPoints, содержит все точки касания, связанный с конкретным событием. TouchPoint, возвращаемый GetPrimaryTouchPoint, всегда представляет основную точку касания. Если с конкретным событием не сопоставлена основная точка касания, GetPrimaryTouchPoint будет возвращать null.
Даже если TouchPoint, возвращенный GetPrimaryTouchPoint, отличен от null, он никогда не совпадает с одним из объектов TouchPoint, возвращаемых GetTouchPoints, хотя все свойства будут одинаковы при передаче в эти методы одного и того же аргумента.
В классе TouchPoint определены следующие четыре свойства только для чтения (все они поддерживаются свойствами зависимостей).
- Action типа TouchAction — это перечисление с членами Down, Move и Up.
- Position типа Point — координаты относительны элементу, переданному методу GetPrimaryTouchPoint или GetTouchPoints в качестве аргумента (или относительны верхнему левому углу основного окна приложения, если вместо аргумента подставлен null).
- Size типа Size. Size типа Size — информация о размере недоступна на PDC-лэптопе, поэтому я вообще не работал с этим свойством.
- TouchDevice типа TouchDevice.
Вы можете вызывать метод SuspendMousePromotionUntilTouchUp из обработчика события, только если GetPrimaryTouchPoint возвращает объект, отличный от null, и свойство Action содержит TouchAction.Down.
Объект TouchDevice содержит два свойства только для чтения, также поддерживаемые свойствами зависимостей.
- DirectlyOver типа UIElement — самый верхний элемент под пальцем.
- ID типа int.
DirectlyOver необязательно должно быть потомком элемента, переданного в GetPrimaryTouchPoint или GetTouchPoints. Это свойство может быть null, если точка касания находится в пределах приложения Silverlight (что определяется размерами подключаемого Silverlight-объекта), но не в области проверки на попадание пальца в какой-либо элемент. (Панели с фоновой кистью, равной null, не проверяются на попадание.)
Свойство ID крайне важно для того, чтобы можно было различать пальцы. Определенная серия событий, сопоставленных с конкретным пальцем, всегда будет начинаться с Action или Down, когда палец касается экрана, затем следуют события Move, а замыкает последовательность событие Up. Все эти события будут связаны с одним ID. (Но не полагайтесь на то, что для основной точки касания значение ID будет 0 или 1.)
Самый нетривиальный код, связанный с обработкой мультисенсорного ввода, будет использовать набор Dictionary, где свойство ID объекта TouchDevice является ключом словаря. Именно так вы будете хранить информацию для конкретной точки касания между событиями.
События
Исследуя новое устройство ввода, всегда полезно написать небольшую программу для регистрации событий на экране, чтобы получить представление, с чем они связаны. В исходном коде, который можно скачать для этой статьи, содержится проект MultiTouchEvents. Он состоит из двух элементов управления TextBox, работающих бок о бок и показывающих мультисенсорные события для двух пальцев. Если у вас есть мультисенсорный монитор, можете запустить эту программу с charlespetzold.com/silverlight/MultiTouchEvents.
XAML-файл включает лишь Grid с двумя колонками и элементами управления TextBox с именами txtbox1 и txtbox2. Его код приведен на рис. 1.
Рис. Код для MultiTouchEvents
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace MultiTouchEvents {
public partial class MainPage : UserControl {
Dictionary<int, TextBox> touchDict =
new Dictionary<int, TextBox>();
public MainPage() {
InitializeComponent();
Touch.FrameReported += OnTouchFrameReported;
}
void OnTouchFrameReported(
object sender, TouchFrameEventArgs args) {
TouchPoint primaryTouchPoint =
args.GetPrimaryTouchPoint(null);
// Inhibit mouse promotion
if (primaryTouchPoint != null &&
primaryTouchPoint.Action == TouchAction.Down)
args.SuspendMousePromotionUntilTouchUp();
TouchPointCollection touchPoints =
args.GetTouchPoints(null);
foreach (TouchPoint touchPoint in touchPoints) {
TextBox txtbox = null;
int id = touchPoint.TouchDevice.Id;
// Limit touch points to 2
if (touchDict.Count == 2 &&
!touchDict.ContainsKey(id)) continue;
switch (touchPoint.Action) {
case TouchAction.Down:
txtbox = touchDict.ContainsValue(txtbox1) ?
txtbox2 : txtbox1;
touchDict.Add(id, txtbox);
break;
case TouchAction.Move:
txtbox = touchDict[id];
break;
case TouchAction.Up:
txtbox = touchDict[id];
touchDict.Remove(id);
break;
}
txtbox.Text += String.Format("{0} {1} {2}\r\n",
touchPoint.TouchDevice.Id, touchPoint.Action,
touchPoint.Position);
txtbox.Select(txtbox.Text.Length, 0);
}
}
}
}
Обратите внимание на определение словаря в начале класса. Словарь отслеживает, какой TextBox сопоставлен со свойствами ID двух точек касания.
Обработчик OnTouchFrameReported начинает с запрета передачи всех событий мыши. Это единственная причина вызова GetPrimaryTouchPoint, и очень часто она такова даже в реальных программах.
В цикле foreach перечисляются члены TouchPoint набора TouchPointCollection, возвращенного GetTouchPoints. Поскольку программа содержит всего два элемента управления TextBox и рассчитана на обработку только двух точек касания, она игнорирует любую точку касания, когда в словаре уже зафиксированы две точки, а ID не находится в этом словаре. (Это необходимо лишь для того, что ваша мультисенсорная программа Silverlight не рухнула из-за обнаружения слишком большого количества точек касания!) ID добавляется в словарь при событии Down и удаляется из него при событии Up.
Вы заметите, что временами элементы управления TextBox засоряются слишком большим количеством текста и что вам нужно выделять весь этот текст и удалять его (Ctrl+A, Ctrl+X), чтобы программа снова выполнялась нормально.
Поиграв с этой программой, вы поймете, что мультисенсорный ввод захватывается на уровне приложения. Например, если вы нажимаете пальцем на области приложения, в затем двигаете его за пределы этого приложения, то программа продолжит принимать события Move и в конечном счете получит событие Up, когда вы поднимете палец. По сути, как только одно приложение начинает принимать мультисенсорный ввод, такой ввод в другие приложения запрещается, и курсор мыши исчезает.
Захват мультисенсорного ввода, ориентированный на приложение, позволяет программе MultiTouchEvents чувствовать себя очень уверенно. Так, при событиях Move и Down программа просто полагает, что ID будет в словаре. В реальных приложениях стоит ввести проверку на случай, если вдруг случится нечто странное, но вы всегда получите событие Down.
Манипуляции двумя пальцами
Один из стандартных вариантов применения мультисенсорного ввода — галерея фотографий, позволяющая перемещать, масштабировать и поворачивать снимки пальцами. Чтобы немного привыкнуть к соответствующим принципам, я решил попробовать нечто в таком духе, но, конечно, попроще. В моей версии программы есть лишь один элемент, которым можно манипулировать, — строка текста «TOUCH». Вы можете запустить программу TwoFingerManipulation на моем сайте charlespetzold.com/silverlight/TwoFingerManipulation.
Кодируя приложения для Multi-Touch, по-видимому, нужно всегда запрещать передачу событий от мыши в элементы управления, поддерживающие мультисенсорный ввод. Но если вы хотите, чтобы ваша программа могла работать и в отсутствие мультисенсорного экрана, то должны добавить специфическую обработку мыши.
Мышью или одним пальцем в программе TwoFingerManipulation все равно можно перемещать строку, но при этом можно менять только ее позицию; эта графическая операция называется преобразованием. Двумя пальцами на мультисенсорном экране объект можно еще и масштабировать или поворачивать.
Сочиняя алгоритм для такого масштабирования и поворота, я вскоре понял, что уникального решения нет!
Допустим, один палец остается в точке ptRef. (Все точки здесь относительны поверхности экрана.) Другой палец движется из точки ptOld в точку ptNew. Как показано на рис. 2, эти три точки можно использовать исключительно для вычисления коэффициентов масштабирования (scaling factors) объекта по горизонтали и вертикали.
Рис. Движение двух пальцев, преобразуемое в коэффициенты масштабирования
Например, масштабирование по горизонтали — это увеличение расстояния ptOld.X и ptNew.X от ptRef.X, или:
scaleX = (ptNew.X – ptRef.X) / (ptOld.X – ptRef.X)
Масштабирование по вертикали аналогично. Для примера на рис. 2 коэффициент масштабирования по горизонтали равен 2, а по вертикали — Ң.
Это, безусловно, более простой способ кодирования. Тем не менее программа работает более естественно, если двумя пальцами можно еще и поворачивать объект (рис. 3)..
Рис. Движение двух пальцев, преобразуемое в поворот и масштабирование
Во-первых, вычисляются углы двух векторов — от ptRef до ptOld и от ptRef до ptNew. (Метод Math.Atan2 идеален для этой работы.) Затем ptOld поворачивается относительно ptRef на разницу этих углов. Далее этот «повернутый» ptOld используется совместно с ptRef и ptNew для расчета коэффициентов масштабирования. Данные коэффициенты масштабирования значительно ниже, поскольку коэффициент поворота был удален.
Реальный алгоритм (реализованный в методе ComputeMoveMatrix в C#-файле) оказался весьма простым. Однако в программе потребуется немало кода, поддерживающего преобразования, для заполнения пробелов в Silverlight-классах преобразований, в которых нет открытого свойства Value или поддержки перемножения матриц (как в WPF).
На практике оба пальца могут двигаться одновременно, и обработка взаимодействия между двумя пальцами проще, чем кажется поначалу. Каждый движущийся палец обрабатывается независимо, используя позицию другого пальца как точку отсчета (reference point). Несмотря на более сложные расчеты, результат выглядит более естественным, и я думаю, что этому есть простое объяснение: на самом деле в жизни пальцами очень часто поворачивают объекты, но крайне редко изменяют размеры чего бы то ни было.
Вращение вообще настолько распространено в реальном мире, что, возможно, имеет смысл реализовать его даже при манипуляциях объектом только одним пальцем или мышью. Это демонстрируется в альтернативной программе AltFingerManipulation (charlespetzold.com/silverlight/AltFingerManipulation). Если вы пользуетесь двумя пальцами, она работает идентично TwoFingerManipulation. А если одним, она вычисляет поворот относительно центра объекта, а любой остаток пути от центра использует для преобразования.
Обертывание события в дополнительные события
Обычно я предпочитаю работать с классами, которые Microsoft тщательно продумывает в своих инфраструктурах, а не обертывать их собственным кодом. Но я имел в виду некоторые мультисенсорные приложения, которые на мой взгляд выиграли бы от более сложного интерфейса событий.
Сначала я хотел создать более модульную систему. Мне нужно было смешать собственные элементы управления, способные обрабатывать свой сенсорный ввод, с существующими элементами управления Silverlight, которые позволяют просто преобразовывать сенсорный ввод в ввод от мыши. Кроме того, я хотел реализовать захват. Хотя приложение Silverlight само захватывает мультисенсорное устройство, мне были нужны индивидуальные элементы управления для независимого захвата конкретной точки касания.
Мне также требовались события Enter и Leave. В некотором смысле эти события противоположны парадигме захвата. Чтобы понять разницу, вообразите на экране фортепианную клавиатуру, на которой каждая клавиша является экземпляром элемента управления PianoKey. Поначалу можно подумать, будто эти клавиши аналогичны кнопкам, срабатывающим при щелчках мышью. При событии MouseDown клавиша пианино начинает звучать, а при событии MouseUp звук прекращается.
Но это вовсе не то, что вы хотели бы от клавиш пианино. Нужна возможность двигать палец по клавиатуре вверх и вниз, чтобы добиться эффектов глиссандо. Для клавиш на самом деле даже не нужны события Down и Up. Реально для них требуются только события Enter и Leave.
В WPF 4 и Microsoft Surface уже есть диспетчеризуемые события касания, и они, весьма вероятно, появятся в Silverlight в будущем. Но для своих нынешних потребностей я создал класс TouchManager, реализованный в проекте библиотеки Petzold.MultiTouch в решении TouchDialDemos. Большая часть TouchManager состоит из статических методов, полей и статического обработчика события Touch.FrameReported, которое позволяет управлять событиями касания в рамках всего приложения.
Класс, которому требуется зарегистрироваться в TouchManager, создает экземпляр так:
TouchManager touchManager = new TouchManager(element);
Аргумент конструктора имеет тип UIElement, и обычно это элемент, создающий объект:
TouchManager touchManager = new TouchManager(this);
Регистрируясь в TouchManager, класс указывает, что он заинтересован во всех мультисенсорных событиях, где свойство DirectlyOver объекта TouchDevice является потомком элемента, переданного в конструктор TouchManager, и что эти мультисенсорные события не должны преобразовываться в события мыши. Отменить регистрацию элемента невозможно.
Создав новый экземпляр TouchManager, класс может установить обработчики для событий с именами TouchDown, TouchMove, TouchUp, TouchEnter, TouchLeave и LostTouchCapture:
touchManager.TouchEnter += OnTouchEnter;
Все обработчики определяются в соответствии с делегатом EventHandler<TouchEventArgs>:
void OnTouchEnter(
object sender, TouchEventArgs args) {
...
}
В TouchEventArgs определены четыре свойства.
- Source типа UIElement, которое элемент изначально передает в конструктор TouchManager.
- Position типа Point. Эта позиция относительна Source.
- DirectlyOver типа UIElement, просто копируемое из объекта TouchDevice.
- ID типа int, тоже просто копируемое из объекта TouchDevice.
Вызывать метод Capture с передачей ID точки касания, сопоставленного с событием, разрешается только при обработке этого события TouchDown:
touchManager.Capture(id);
Весь дальнейший сенсорный ввод для данного ID посылается элементу, связанному с этим экземпляром TouchManager, пока не появится событие TouchUp или не будет явно вызван ReleaseTouchCapture. В любом случае TouchManager в ответ сгенерирует событие LostTouchCapture.
События обычно имеют следующий порядок: TouchEnter, TouchDown, TouchMove, TouchUp, TouchLeave и LostTouchCapture (если применимо). Разумеется, между TouchDown и TouchUp может быть множество событий TouchMove. Когда точка касания не захвачена, может возникать множество событий в порядке TouchLeave, TouchEnter и TouchMove по мере того, как точка касания покидает один зарегистрированный элемент и входит в другой.
Элемент управления TouchDial
Изменения в парадигме пользовательского ввода часто требуют пересмотра подхода к проектированию элементов управления и других механизмов ввода. Например, немногие GUI-элементы так же укоренились, как полоса прокрутки и ползунок. С помощью этих элементов управления вы не только осуществляете навигацию по большим документам или изображениям, но и используете как крошечные элементы регулировки громкости звука в мультимедийных плеерах.
Когда я подумывал создать на экране элемент управления громкостью, который реагировал бы на касание, я заинтересовался, а годится ли здесь старый подход. На практике ползунки иногда используются в качестве регуляторов громкости, но обычно они встречаются на профессиональных микшерах и графических эквалайзерах. Большинство регуляторов громкости в реальной жизни являются дисковыми. Может ли дисковая шкала стать более эффективным решением для элемента управления громкостью с поддержкой сенсорного ввода?
Не стану притворяться, будто у меня есть определенный ответ, но покажу, как создать такой элемент управления.
Элемент управления TouchDial включен в библиотеку Petzold.MultiTouch в решении TouchDialDemos (детали см. в полном исходном коде). TouchDial наследует от RangeBase, поэтому он может использовать преимущества события ValueChanged и свойств Minimum, Maximum и Value, включая логику удержания значения Value в диапазоне между Minimum и Maximum. Но в TouchDial свойства Minimum, Maximum и Value содержат углы в градусах.
TouchDial реагирует как на мышь, так и на касание и использует класс TouchManager для захвата точки касания. При вводе мышью или сенсорном вводе TouchDial изменяет значение свойства Value в течение события Move, исходя из новой и предыдущей позиций курсора мыши или пальца относительно центральной точки. Это действие сильно напоминает то, которое показано на рис. 3, с тем исключением, что здесь не происходит масштабирования. Обработчик события Move использует метод Math.Atan2 для преобразования декартовой системы координат в углы, а затем добавляет разницу между двумя углами к Value.
В TouchDial нет шаблона по умолчанию, поэтому у него нет исходного внешнего вида. Предоставить шаблон должен программист, использующий TouchDial, но он может быть достаточно простым и содержать всего несколько элементов. Очевидно, что какой-то элемент в этом шаблоне должен поворачиваться в соответствии с изменениями в свойстве Value. Для удобства TouchDial предоставляет свойство RotateTransform только для чтения; при этом свойство Angle равно свойству Value класса RangeBase, а свойства CenterX и CenterY отражают центральную точку элемента управления.
На рис. 4 показан XAML-файл с двумя элементами TouchDial, которые ссылаются на стиль и шаблон, определенные как ресурс.
Рис. XAML-файл для проекта SimpleTouchDialTemplate
<UserControl x:Class="SimpleTouchDialTemplate.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
<UserControl.Resources>
<Style x:Key="touchDialStyle"
TargetType="multitouch:TouchDial">
<Setter Property="Maximum" Value="180" />
<Setter Property="Minimum" Value="-180" />
<Setter Property="Width" Value="200" />
<Setter Property="Height" Value="200" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="multitouch:TouchDial">
<Grid>
<Ellipse Fill="{TemplateBinding Background}" />
<Grid RenderTransform="{TemplateBinding RotateTransform}">
<Rectangle Width="20" Margin="10"
Fill="{TemplateBinding Foreground}" />
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<multitouch:TouchDial Grid.Column="0"
Background="Blue" Foreground="Pink"
Style="{StaticResource touchDialStyle}" />
<multitouch:TouchDial Grid.Column="1"
Background="Red" Foreground="Aqua"
Style="{StaticResource touchDialStyle}" />
</Grid>
</UserControl>
Заметьте, что стиль устанавливает свойство Maximum в 180, а свойство Minimum в –180, чтобы полоску можно было вращать на 180 градусов влево и вправо. (Как ни странно, программа работала некорректно, если я переключал порядок этих двух свойств в определении стиля.) Регулятор состоит просто из полоски, образованной элементом Rectangle внутри Ellipse. Элемент Bar находится внутри одноячейковой сетки Grid, свойство RenderTransform которой привязано к свойству RotateTransform, вычисляемому TouchDial.
Программа SimpleTouchDialTemplate в действии показана на рис. 5.
Рис. Программа SimpleTouchDialTemplate в действии
Вы можете запустить эту программу (и две следующих) с charlespetzold.com/silverlight/TouchDialDemos.
Поворачивать полоску внутри окружности мышью не очень удобно, а пальцем гораздо естественнее. Заметьте, что вы можете поворачивать полоску, если нажали левую кнопку мыши (или коснулись пальцем экрана) в любой точке внутри окружности. Но, уже поворачивая полоску, вы можете переместить курсор мыши или палец за пределы окружности, так как захватываются оба типа устройств ввода.
Если вы хотите запретить поворот полоски, если курсор мыши или палец не находятся непосредственно на полоске, установите свойство IsHitTestVisible объекта Ellipse в False.
Моя первая версия TouchDial не включала свойство RotateTransform. Я решил, что разумнее ввести это свойство в шаблон, где свойство Angle было бы связано через TemplateBinding со свойством Value элемента управления. Однако в Silverlight 3 привязки к свойствам классов, производных не от FrameworkElement, не работают, поэтому свойство Angle свойства RotateTransform не может выступать в роли мишени привязки (эта проблема устранена в Silverlight 4).
Поворот всегда осуществляется относительно центральной точки, и этот факт усложняет элемент управления TouchDial. TouchDial использует центральную точку для двух целей: для вычисления углов, показанных на рис. 3, и для задания свойств CenterX и CenterY создаваемого им RotateTransform. По умолчанию TouchDial вычисляет обе координаты центра как половины значений свойств ActualWidth и ActualHeight, но в очень многих случаях это не совсем то, что нужно.
Например, в шаблоне на рис. 4 вы хотите связать свойство RenderTransform объекта Rectangle со свойством RotateTransform элемента управления TouchDial. Такая привязка будет работать некорректно, так как TouchDial устанавливает свойства CenterX и CenterY свойства RotateTransform в 100, а центр Rectangle относительно самого себя на самом деле находится в точке (10, 90). Чтобы вы могли переопределять исходные значения, которые TouchDial вычисляет по размерам элемента управления, в нем определены свойства RenderCenterX и RenderCenterY. В свойстве SimpleTouchDialTemplate вы можете задать эти свойства в таком стиле:
<Setter Property="RenderCenterX" Value="10" />
<Setter Property="RenderCenterY" Value="90" />
Или присвоить этим свойствам нулевые значения и задать RenderTransformOrigin элемента Rectangle, чтобы указать центр относительно него:
RenderTransformOrigin="0.5 0.5"
Кроме того, вам может понадобиться использование TouchDial в тех случаях, где точка отсчета для перемещений курсора мыши или пальца не находится в центре элемента управления. Тогда вы можете задать свойства InputCenterX и InputCenterY, чтобы переопределить исходные значения.
На рис. 6 показан XAML-файл проекта OffCenterTouchDial.
Рис. XAML-файл проекта OffCenterTouchDial
<UserControl x:Class="OffCenterTouchDial.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
<Grid x:Name="LayoutRoot">
<multitouch:TouchDial Width="300" Height="200"
HorizontalAlignment="Center" VerticalAlignment="Center"
Minimum="-20" Maximum="20"
InputCenterX="35" InputCenterY="100"
RenderCenterX="15" RenderCenterY="15">
<multitouch:TouchDial.Template>
<ControlTemplate TargetType="multitouch:TouchDial">
<Grid Background="Pink">
<Rectangle Height="30" Width="260"
RadiusX="15" RadiusY="15" Fill="Lime"
RenderTransform="{TemplateBinding RotateTransform}" />
<Ellipse Width="10" Height="10"
Fill="Black" HorizontalAlignment="Left"
Margin="30" />
</Grid>
</ControlTemplate>
</multitouch:TouchDial.Template>
</multitouch:TouchDial>
</Grid>
</UserControl>
Этот файл содержит единственный элемент управления TouchDial, где свойства заданы в самом элементе управления, а свойство Template указывает на шаблон Control, содержащий одноячейковую сетку Grid с Rectangle и Ellipse. Ellipse — это крошечная символическая точка вращения для Rectangle, который можно поворачивать вверх или вниз на 20 градусов (рис. 7).
Рис. Программа OffCenterTouchDial в действии
Значения свойств InputCenterX и InputCenterY всегда относительны элементу управления в целом, поэтому они указывают позицию центра элемента Ellipse внутри розовой сетки Grid. Значения свойств RenderCenterX и RenderCenterY всегда относительны той части элемента управления, к которой применяется свойство RotateTransform.
Элементы управления громкостью и высотой звука
Два предыдущих примера продемонстрировали, как можно придать внешний вид элементу управления TouchDial явным заданием свойства Template в разметке или, если вам нужно совместно использовать шаблоны для нескольких элементов управления, ссылкой на ControlTemplate, определенный как ресурс.
Кроме того, вы можете создать новый класс, производный от TouchDial, и использовать для задания шаблона исключительно XAML-файл. Что и происходит с RidgedTouchDial в библиотеке Petzold.MultiTouch. Библиотека RidgedTouchDial не отличается от TouchDial за исключением характерного для нее размера и внешнего вида (что будет скоро продемонстрировано).
Также можно использовать TouchDial (или производный класс вроде RidgedTouchDial) в классе, производном от UserControl. Преимущество такого подхода в том, что вы можете скрывать все свойства, определенные RangeBase, в том числе Minimum, Maximum и Value, и заменять их новым свойством.
Такой вариант применяется в VolumeControl. VolumeControl наследует от RidgedTouchDial свой внешний вид и определяет новое свойство Volume. Это свойство поддерживается свойством зависимости и любые изменения в Volume генерируют событие VolumeChanged.
XAML-файл для VolumeControl просто ссылается на RidgedTouchDial и задает несколько свойств, включая Minimum, Maximum и Value:
<src:RidgedTouchDial
Name="touchDial"
Background="{Binding Background}"
Maximum="150"
Minimum="-150"
Value="-150"
ValueChanged="OnTouchDialValueChanged" />
Таким образом, TouchDial можно поворачивать на 300 градусов из минимальной позиции в максимальную. Элемент управления преобразует диапазон 300 градусов в логарифмическую шкалу в децибелах от 0 до 96.
Рис. C#-файл для VolumeControl
using System;
using System.Windows;
using System.Windows.Controls;
namespace Petzold.MultiTouch {
public partial class VolumeControl : UserControl {
public static readonly DependencyProperty VolumeProperty =
DependencyProperty.Register("Volume",
typeof(double),
typeof(VolumeControl),
new PropertyMetadata(0.0, OnVolumeChanged));
public event DependencyPropertyChangedEventHandler VolumeChanged;
public VolumeControl() {
DataContext = this;
InitializeComponent();
}
public double Volume {
set { SetValue(VolumeProperty, value); }
get { return (double)GetValue(VolumeProperty); }
}
void OnTouchDialValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> args) {
Volume = 96 * (args.NewValue + 150) / 300;
}
static void OnVolumeChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args) {
(obj as VolumeControl).OnVolumeChanged(args);
}
protected virtual void OnVolumeChanged(
DependencyPropertyChangedEventArgs args) {
touchDial.Value = 300 * Volume / 96 - 150;
if (VolumeChanged != null)
VolumeChanged(this, args);
}
}
}
Почему именно 96? Ну, хотя шкала в децибелах основана на десятичных числах, всякий раз, когда амплитуда сигнала увеличивается на мультипликативный множитель, равный 10, громкость возрастает линейно на 20 децибел, — но верно и то, что 10 в третьей степени примерно соответствует 2 в 10-й степени. То есть, когда величина амплитуды удваивается, громкость увеличивается на 6 децибел. Следовательно, если вы представите амплитуду 16-битным числом (что и имеет место в случае аудио компакт-дисков и звука на ПК), то получите диапазон 16 бит…6 децибел/бит, или 96 децибел.
Класс PitchPipeControl также наследует от UserControl и определяет новое свойство Frequency. XAML-файл включает элемент управления TouchDial и целый букет элементов управления TextBlock для отображения 12 нот октавы. PitchPipeControl использует и другое свойство TouchDial, о котором я еще не рассказывал: если вы задаете для SnapIncrement ненулевое значение в градусах, движение поворотной шкалы будет неплавным. Поскольку PitchPipeControl можно настроить на 12 нот октавы, SnapIncrement присваивается 30 градусов.
На рис. 9 показана программа PitchPipe, в которой скомбинированы VolumeControl и PitchPipeControl. Вы можете запустить ее с charlespetzold.com/silverlight/TouchDialDemos.
Рис. Программа PitchPipe
Бонусная программа
Ранее в этой статье я упомянул элемент управления PianoKey в контексте одного из примеров. PianoKey является настоящим элементом управления и является одним из нескольких элементов в программе Piano, которую вы можете запустить с charlespetzold.com/silverlight/Piano. Эта программа рассчитана на отображение в полностью раскрытом окне браузера. (Или нажмите клавишу F11, чтобы перевести Internet Explorer в полноэкранный режим и получить еще больше места.) Совсем крошечный вариант показан на рис. 10. Клавиатура делится на перекрывающиеся части более высокой и низкой октав. Точки обозначают ноту до средней октавы (Middle C).
Рис. Программа Piano
Именно для этой программы я написал TouchManager, потому что она использует мультисенсорный ввод тремя способами. Я уже рассматривал VolumeControl, который захватывает точку касания по событию TouchDown и освобождает ее по событию TouchUp. Элементы управления PianoKey, образующие клавиатуру, тоже используют TouchManager, но эти элементы слушают только события TouchEnter и TouchLeave. Вы реально можете провести пальцами по клавишам и получить эффекты глиссандо. Темные прямоугольники, действующие как педали фортепиано, являются обыкновенными Silverlight-элементами управления ToggleButton. В них нет специфической поддержки мультисенсорного ввода; вместо это точки касания преобразуются в события мыши.
Программа Piano демонстрирует три способа использования мультисенсорного ввода. Подозреваю, что их может быть намного больше. Подозреваю, что их может быть намного больше.