Department of InformatiX
Microsoft .NET Micro Framework Tools & Resources

Tlačíto není klávesa

Pravděpodobně tuto důležitou skutečnost ještě párkrát uslyšíte: tlačítko není klávesa. Jeho účelem není ani vkládat znaky ani číslice. Ve Windows Presentation Foundation je spíš podobný konceptu příkazů (Commands) než klávesnicovému vstupu (nicméně, tlačítko není ani příkaz). Předchůdce .NET Micro Frameworku - SPOT běžící např. ve smart watches - nepřijímá od uživatele žádný textový vstup, a to je také důvod, proč máme trasované události ButtonDown a ButtonUp místo KeyDown resp. KeyUp.

Enum Microsoft.SPOT.Input.Button (ve verzi 2.x) reprezentuje všehovšudy 13 tlačítek: Menu, Select, Up, Down, Left, Right, Play, Pause, FastForward, Rewind, Stop, Back a Home. Samozřejmě, pokud vyrábíte nějaké dálkové ovládání či něco zabezpečeného vyžadujícího od uživatele přístupový kód, pak to nejspíš nebude stačit. Je však důležité si uvědomit, že vaším problémem není malý počet tlačítek, vaším problémem v první řadě je, že tu nejsou vůbec žádné klávesy! Když se nad tím chvilku zamyslíte, sotva nějaké zařízení (nevyžadující uživatelův vstup) potřebuje více než třináct tlačítek. A viděli jste ty hodinky? Můžeme být rádi, že jich vůbec máme definováno takovou spoustu...

Předešlý odstavec však příliš neplatí pro .NET Micro Framework 3.0 - zde máme definováno téměř 300 tlačítek. Jsem ale zásadně proti způsobu, jakým to bylo provedeno a tento článek alespoň může ukázat, jak snadné je konstanty přejmenovat.

Rozšíření Button enumu

Porozumět problému je důležitá věc. Teď se ale podívejme, co s tím můžeme udělat už dnes.

Především je dobré si uvědomit, že nepotřebujete používat hodnoty jako Play nebo Menu. Pokud vaše zařízení má jedno zelené a jedno červené tlačítko, není nutné kvůli tomu seznam tlačítek rozšiřovat - prostě si svá tlačítka nadeklarujte a přiřaďte jim některé předdefinované, nezáleží na tom které - koneckonců jsou to jen čísla...

public static class BezvaButton { public const Button Rudé = Button.FastForward; public const Button Zelené = Button.Home; }

Pokud nepotřebujete dohromady více než 13 tlačítek, pak tento trik bude všude procházet bez dramatických změn aplikace:

Za druhé, je to běžný Enum jako každý jiný. To znamená, že může nabývat libovolné hodnoty typu, který dědí (int). Takže technicky vzato jím můžete reprezentovat až 4294967294 tlačítek. Pro uložení libovolného čísla v enumu je třeba použít explicitní cast:

Button FialovýSKytičkama = (Button)12345;

 

Následující platí pouze pro framework 2.x. Díky za vaše hlasy!

Používání takového tlačítka však vyžaduje trochu kódu navíc. Pokud se ho totiž pokusíte podstrčit při vytváření ButtonEventArgs, budete odměněni ArgumentOutOfRangeException výjimkou.

UncheckedButtonEventArgs

Já osobně nevidím jediný důvod, proč by ButtonEventArgs měla klást jakékoliv požadavky na hodnotu tlačítka, ale aspoň není sealed, takže si můžeme vytvořit vlastní a vydědit ji. :)

Jsou tu ale dva vážné problémy. Ten první je, že ButtonEventArgs.Button není vlastnost, takže nemůže být při dědění pozměněna (kromě toho stejně pochybuji, že by bývala byla označena jako virtuální). Druhá potíž spočívá v tom, že je proměnná deklarována jako readonly, jinými slovy, lze do ní zapsat pouze v konstruktoru. Takže zbývá ta nejhorší možná cesta - musíme ji překrýt novou proměnnou:

public class UncheckedButtonEventArgs : ButtonEventArgs { private readonly Button _button; public new Button Button { get { return _button; } } public UncheckedButtonEventArgs(PresentationSource source, TimeSpan timestamp, Button button) : base(null /* buttonDevice */, source, timestamp, (button > Button.None && button <= Button.Last) ? button : Button.Last) { _button = button; } }

Z důvodu záchování alespoň nějaké kompatibility nastavíme Button báze na požadovanou hodnotu pokud výjimku nezpůsobí, v opačném případě na Button.Last. Parametr buttonDevice se vůbec nevyužívá, takže jej nastavuji na null, i když si nejsem jistý, jak moc dobré je to rozhodnutí do budoucna.

UncheckedButtonEventArgs sama o sobě ale nestačí. Protože schováváme člen báze, musíme v aplikaci pracovat přímo s typem UncheckedButtonEventArgs, abychom se dostali k naší deklaraci. To se dá při zpracování události zajistit např. tímto snadným testem:

protected override void OnButtonDown(ButtonEventArgs e) { Button button = e is UncheckedButtonEventArgs ? (e as UncheckedButtonEventArgs).Button : e.Button; ... }

Integrace do systému vstupu WPF

Používání GPIOButtonInputProvideru je maličko složitější. Otevřete si jej a najděte třídu ButtonPad s metodou Interrupt, která vypadá asi takto:

void Interrupt(Cpu.Pin port, bool state, TimeSpan time) { RawButtonActions action = state ? RawButtonActions.ButtonUp : RawButtonActions.ButtonDown; RawButtonInputReport report = new RawButtonInputReport(sink.source, time, button, action); // Queue the button press to the input provider site. sink.Dispatcher.BeginInvoke(sink.callback, report); }

To je poslední stádium při kterém je o změně stavu tlačítka připravena zpráva pro vstupní systém a odeslána do WPF k šíření a zpracování. Dispatcher pak zavolá sink.callback (což je delegát k metodě InputProviderSite.ReportInput) což jednoduše řečeno způsobí vyvolání a trasování událostí PreviewButtonDown a ButtonDown. Bohužel, ButtonEventArgs je vytvářena hluboko uvnitř tohoto procesu a nelze to jen tak změnit. Navíc, protože není možné v první verzi WPF vstupní systém nijak rozšiřovat, nemůžeme ani celý proces vyměnit za jiný.

Ještě ale není vše ztraceno. Nemůžeme se sice vpašovat do WPF, ale můžeme alespoň vyvolat události ručně. ButtonDown a ButtonUp jsou trasované události, takže je potřebujeme vyvolat po celém stromu ovládacích prvků. To je přesně to, co dělá metoda UIElement.RaiseEvent(RoutedEventArgs args). Zbývá už jen rozhodnout, na kterém ovládacím prvku ji zavoláme a to je snadné - na tom, který má právě fokus. Teď už jsme schopni dát dohromady metodu (a její popis delegáta), kterou musí dispatcher zavolat:

private delegate void RaiseEventCallback(RoutedEventArgs args); private void RaiseEvent(RoutedEventArgs args) { UIElement target = Buttons.FocusedElement; if (target != null) target.RaiseEvent(args); }

Vložte ji přímo do třídy GPIOButtonInputProvider, a vytvořte také delegáta, který na ni ukazuje:

public sealed class GPIOButtonInputProvider { ... private ReportInputCallback callback; // stávající delegát do InputSite private RaiseEventCallback eventCallback; // delegát do naší metody // This class maps GPIOs to Buttons processable by Microsoft.SPOT.Presentation public GPIOButtonInputProvider(PresentationSource source) { ... callback = new ReportInputCallback(site.ReportInput); // stávající delegát eventCallback = new RaiseEventCallback(RaiseEvent); // a náš nový ... }

A nakonec upravíme výše uvedenou Interrupt rutinku (ve třídě ButtonPad) tak, aby tlačítka mimo povolený rozsah zasílala naší metodě místo do InputProviderSite:

... // Queue the button press to the input provider site. if (button > Button.None && button <= Button.Last) sink.Dispatcher.BeginInvoke(sink.callback, report); else { RoutedEventArgs args = new UncheckedButtonEventArgs(null, time, button); args.RoutedEvent = state ? Buttons.ButtonDownEvent : Buttons.ButtonUpEvent; sink.Dispatcher.BeginInvoke(sink.eventCallback, args); } }

Tak, všechno potřebné je už zařízeno, a můžete nyní v provideru používat svá skvělá tlačítka:

... ButtonPad[] buttons = new ButtonPad[] { // Associate the buttons to the pins as setup in the emulator/hardware new ButtonPad(this, Button.Up , Cpu.Pin.GPIO_Pin0), new ButtonPad(this, BezvaButton.FialovýSKytičkama , Cpu.Pin.GPIO_Pin1), }; ...

A má poslední poznámka pro dnešek: Mějte na paměti, že toto je pouze jednoduchá záplata. Pokud byste to chtěli udělat skutečně patřičně dobře, měli byste také nejdříve vyvolat příslušné preview události, trasovat je stromem opačným směrem a zkontrolovat, zda již v nich nebylo tlačítko zpracováno, než pustíte do světa "opravdové" události. Mějte se hezky!

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