Department of InformatiX
Microsoft .NET Micro Framework Tools & Resources
Ve verzi 4.0 byl tento bug opraven.

Inspirován dotazem z diskusní skupiny, přináším ukázku toho, jak dopravit dotykové události do okna, do kterého patří.

Bug v praxi

Nejdříve si předveďme nějakou aplikaci, která by bug demonstrovala:

using Microsoft.SPOT; using Microsoft.SPOT.Input; using Microsoft.SPOT.Presentation; using Microsoft.SPOT.Presentation.Media; using Microsoft.SPOT.Touch; using TouchWindows { public class MyApplication : Application { public static MyApplication MyCurrent; public static void Main() { MyCurrent = new MyApplication(); Touch.Initialize(MyCurrent); MyCurrent.Run(); } public MyApplication() { for (int i = 0; i < 5; i++) new MyWindow { Index = i, Height = SystemMetrics.ScreenHeight, Width = SystemMetrics.ScreenWidth, Background = new SolidColorBrush(ColorUtility.ColorFromRGB( (byte)(255 * (i & 1)), (byte)(127 * (i & 2)), (byte)(63 * (i & 4)) )), Visibility = Visibility.Visible, }; } } public class MyWindow : Window { protected override void OnStylusDown(StylusEventArgs e) { // Topmost = false; MyApplication.MyCurrent.Windows[(Index + 1) % MyApplication.MyCurrent.Windows.Coun t].Topmost = true; } public int Index; }

Toto je v celku standardní kód jak spouštět aplikaci, velmi podobný tomu, který obdržíte vygenerovaný při zakládání nové windows aplikace. Během vzniku vytvoří aplikace 5 prázdných oken přes celou obrazovku, které se liší barvou pozadí. Okna jsou automaticky přiřazena aplikaci a první z nich je nastaveno jako hlavní okno aplikace (to vše zařizuje konstruktor Windows). V příkladě si také pamatujeme statický odkaz na instanci aplikace (MyApplication.MyCurrent) a to v podstatě proto, abychom se mohli vyhnout neustálému přetypovávání a měli tak čitelnější kód.

Hlavním smyslem MyWindow je přinést následující okno před všechna ostatní. Kdyby byla vlastnost Window.Tompost implementována pořádně (momentálně vždy předpokládá, že zapisujete true), byl by tento úkol celkem snadný. Takto si však musíme pomoci sami, zavést proměnnou Index a nastavit Topmost = true na okně dalším (Index + 1).

Okna jsou vytvořena v tomto pořadí:

01234

Když aplikaci spustíte, uvidíte modré okno. To proto, že modré okno je poslední přidané. Pokud na něj nyní kliknete (no jasan, klepněte prstem! ...věděl jsem, že to zkoušíte na hardware ;-)), uvidíte červené okno. Jak to? Důvodem je, protože hlavní okno aplikace (MainWindow) je ve výchozím případě první vytvořené okno (černé), a pouze hlavní okno dostává dotykové události, i kdyby nebylo vůbec viditelné! OnStyleDown handler černého okna dává do popředí další, tedy červené okno. Pokud kliknete znovu, nic se nezmění - stále je to totiž černé okno, kdo událost dostane:

4111...

Takto se tedy bug projevuje.

Jak funguje dotek

Ve vstupním bodě programu je jeden důležitý řádek: Touch.Initialize(MyCurrent). Tato metoda registruje třídu typu IEventListener jako odběratele dotykových událostí. Seznam takových odběratelů udržuje (tak trochu statická) třída Microsoft.SPOT.EventSink (dědící Microsoft.SPOT.Hardware.NativeEventDispatcher). Když se dotknete displeje, dotykový ovladač vyvolá událost NativeEventDispatcher.OnInterrupt, čímž se informace dostane do světa řízeného kódu.

Život události

Ve skutečnosti třída EventSink udržuje tři seznamy: zpracovávatelů (processors), filtrů (filters) a odběratelů (listeners), pro každou kategorii událostí (gesta, síť, úložiště, dotyky, atd.). Každá taková kategorie může mít pouze jednoho zpracovávatele, pouze jeden filtr a pouze jednoho odběratele. Pokud vy nebo výrobce hardware zavedete nějakou svoji kategorii, můžete jako EventCategory použít libovolné volné číslo. V takovém případě většinou existuje statická třída se standardními událostmi (jako např. NetworkChange nebo RemovableMedia), ke kterým lze přihlásit více handlerů. V případě dotykových událostí byl však použit jiný návrh, k tomu se dostaneme za chviličku. Úkolem zpracovávatelů je vydolovat z 64 bitů nativních dat (z nichž 8 je rezervováno pro kategorii události) smysluplné informace a vrátit je ve formě instance nějaké Microsoft.SPOT.BaseEvent. Pokud pro danou kategorii takový zpracovávač neexistuje, vytvoří se GenericEvent s předpokladem, že data představují X a Y souřadnice na displeji. Tato data jsou pak předána filtru, který vrací true, má-li se pokračovat ve zpracovávání události, nebo false v opačném případě. Takový filtr například používá ovládací prvek InkCanvas ke sběru dat pouze uvnitř jeho hranic. Filtry a odběratelé používají stejnou signaturu, resp. rozhraní (existuje jen IEventListener) - to je důvod, proč příklady v tomto článku musí vracet nějakou boolovskou hodnotu. Výsledek se v případě odběratelů nikde nepoužívá, ale to se může v budoucnu změnit. Odběratel pak představuje příjemce události, něco jako běžný handler.

Třída Application implementuje rozhraní IEventListener, takže všechny WPF aplikace (včetně MyApplication v příkladu výše) mohou přijímat události. To se používá právě při zpracování dotykových událostí, a volání Touch.Initialize(MyCurrent) právě registruje aplikaci jako odběratele této kategorie událostí. Když se dotknete displeje, aplikace obdrží událost, vytvoří Microsoft.SPOT.Input.InputReport a odešle jej do vstupního systému WPF. V případě dotyku také nastaví Microsoft.SPOT.Input.InputManager.StylusWindow na ovládací prvek, na kterém budou vyvolány odpovídající stylusové události. To je to místo, kde se bug nachází, neboť tato vlastnost je natvrdo nastavena na hodnotu MainWindow místo okna v popředí:

public bool OnEvent(BaseEvent ev) { StylusEvent stylusEvent = (StylusEvent)ev; UIElement capturedElement = Stylus.Captured; if (capturedElement == null && MainWindow != null) capturedElement = MainWindow.ChildElementFromPoint(stylusEvent.X, stylusEvent.Y); InputManager.CurrentInputManager.StylusWindow = capturedElement ?? MainWindow; Dispatcher.BeginInvoke(_inputCallback, // odkaz na metodu InputProviderSite.ReportInput new object[] { new RawStylusInputReport(null, stylusEvent.Time, stylusEvent.EventMessage, stylusEvent.X, stylusEvent.Y) }); return true; }

(kód je zjednodušen, ve skutečnosti zpracovává i ostatní události)

Náhrada vyžaduje trocha reflexe

Přestože metoda Application.OnEvent není virtuální, zdá se, že v případě rozhraní se používají metody daného typu, takže by stačilo původní implementaci jen schovat pomocí klíčového slova new. To je však velmi špinavé řešení a tak si ho neukážeme. Udělejme to profesionálně, universálně a rozšířitelně - pomocí naší vlastní třídy typu IEventListener:

using System; using Microsoft.SPOT; using Microsoft.SPOT.Input; using Microsoft.SPOT.Presentation; public class TopmostListener : IEventListener { private Application _application; // pro zlepšení výkonu vše cachujeme private InputManager _inputManager; private ReportInputCallback _inputCallback; public virtual void InitializeForEventSource() { _application = Application.Current; if (_application == null) throw new InvalidOperationException("Vyžadována aplikace."); _application.InitializeForEventSource(); // nezapomeňte nejdříve inicializovat aplikaci _inputManager = InputManager.CurrentInputManager; _inputCallback = new ReportInputCallback(_inputManager.RegisterInputProvider(this).ReportInput); } public virtual bool OnEvent(BaseEvent ev) { StylusEvent stylusEvent = ev as StylusEvent; if (stylusEvent == null) return _application.OnEvent(ev); // ostatní události nepotřebujeme opravovat, takže to necháme na původní implementaci UIElement capturedElement = Stylus.Captured; if (capturedElement == null && _application.MainWindow != null) capturedElement = GetTopmostWindow().ChildElementFromPoint(stylusEvent.X, stylusEvent.Y); _inputManager.StylusWindow = capturedElement ?? GetTopmostWindow(); _application.Dispatcher.BeginInvoke(_inputCallback, // odkaz na metodu InputProviderSite.ReportInput new object[] { new RawStylusInputReport(null, stylusEvent.Time, stylusEvent.EventMessage, stylusEvent.X, stylusEvent.Y) }); return true; } public Window GetTopmostWindow() { ... } private delegate bool ReportInputCallback(InputReport inputReport); }

Použití takového odběratele je celkem snadné, stačí změna v prvních řádcích programu:

public static void Main() { MyCurrent = new MyApplication(); Touch.Initialize(new TopmostListener()); MyCurrent.Run(); }

Tak, poslední netriviální věc, která zbývá, je zjištění okna v popředí. Podívejme se, jak v .NET Micro Frameworku funguje správa oken. Seznamy oken jsou udržovány na dvou místech - v aplikaci a ve správci oken. V aplikaci je jeden seznam zvlášť pro okna, která k aplikaci patří a zvlášť jeden pro ostatní, ačkoliv jsem pro to ve stávající verzi frameworku nenašel žádné využití. Oproti tomu (tak trochu statický) WindowManager má jednotný seznam všech oken která se na zařízení vytvoří, a také sám o sobě okna umisťuje a vykresluje, jelikož se technicky jedná o ovládací prvek Canvas:

Struktura Window a WindowManageru

Takže abychom nalezli okno v popředí, potřebujeme najít posledního logického potomka WindowManageru (ten je totiž vykreslován jako poslední a vypadá tak, že je navrchu). Nastavíte-li vlastnost Topmost na hodnotu true, správce oken pouze posune okno na konec kolekce svých potomků. Pokud jste se seznámeni s WPF v .NET Frameworku, již víte co potřebujeme - pomocníka pro logický strom :)) pro procházení stromů ovládacích prvků. Žádného takového však v .NET Micro Frameworku nemáme, takže si ho musíme napsat sami. Nevýhoda v tvorbě vlastního pomocníka je, že nemá přístup k interním vlastnostem frameworku, takže k získání kolekce potomků potřebujeme malou pomoc reflexe:

using System.Reflection; using Microsoft.SPOT.Presentation; public static class LogicalTreeHelper { private static FieldInfo _logicalChildren = typeof(UIElement).GetField("_logicalChildren", BindingFlags.NonPublic | BindingFlags.Instance); public static UIElementCollection GetChildren(UIElement element) { return (UIElementCollection)_logicalChildren.GetValue(element); } public static UIElement GetParent(UIElement element) { return element.Parent; } }

Nyní je již zřejmé, jak okno v popředí získat:

public Window GetTopmostWindow() { UIElementCollection windows = LogicalTreeHelper.GetChildren(WindowManager.Instance); if (windows == null) return _application.MainWindow; // návrat k výchozímu chování frameworku int i = windows.Count; // kontrola viditelnosti, while (--i >= 0) // zasílání dotykových událostí do neviditelných oken také nemá moc smysl { Window window = (Window)windows[i]; if (window.Visibility == Visibility.Visible) return window; } return _application.MainWindow; // žádné viditelné okno nemáme, tak zpět k výchozímu }

A je to všechno. Když nyní aplikaci spustíte, můžete procházet okny tak, jak jsme původně chtěli - vždy jen na předním okně budou vyvolány patřičné události:

401234...

 

PS: Tímto však nespravíme metody Microsoft.SPOT.Input.Stylus.Capture, které zůstávají k dispozici pouze pro ovládací prvky hlavního okna aplikace. Nicméně když už si děláme vlastního odběratele, tak jistě nebude problém nahradit vlastním řešením i tuto funkcionalitu.

Comments
Sign in using Live ID to be able to post comments.