dotNed

Welkom bij dotNed Inloggen | Aanmelden | Help
in Zoeken

Dennis' avonturen in .net

C#, C++, MessageLoops, P/Invoke en CLR Types: zucht.

Bij Detrio maken we WPF software. Die software moet bediend kunnen worden vanaf een touchscreen. In Windows 7 en WPF 4.0 zit dat standaard ingebouwd; je hebt alleen de juiste hardware nodig. En die is gewoon te koop.

Prima.

Maar.... Windows 7 is nog in beta en WPF 4.0 is nog lang niet in zicht. En als dat al wel zo was dan duurt het nog een hele tijd voor het bedrijfsleven er aan wil. Helaas, maar het is niet anders. We moeten het dus doen met de spullen die nu op de markt zijn.

Op kantoor heb ik de beschikking over HP hardware. Een daarvan is de TX2 laptop, met inderdaad een multi-touch scherm. In Vista is het gebruik ervan uitgeschakeld: Vista herkent maar een touch tegelijk. Ok, bij de HP zit software om foto's te bekijken en dat soort dingen die wel gebruik maken van multi-touch maar echt indrukwekkend is dat allemaal niet.

Ik wil uiteraard mijn applicaties ook gebruik laten maken van de multi-touch mogelijkheden. Dus ik ben op een zoektocht gegaan naar hoe dat moet. Uiteindelijk kwam ik op de site van de leverancier van het scherm voor mijn TX2 terecht. Deze leverde een SDK waarmee ISV's (dat zijn wij dus, de independent software vendors) hun applicatie multi-touch enabled kunnen maken onder Vista.

Helaas was alle code die er bij geleved werd in C++ en dat is toch niet echt meer mijn taaltje. Er zat ook een DLL bij en die kan ik vanuit C# prima gebruiken. Ik weet nog genoeg van C++ om de voorbeelden om te kunnen zetten in C# dus vol goede moed ging ik aan de gang.

Stappen voor het verwerken van multi-touch events

In eerste instantie zag het er allemaal heel eenvoudig uit. In principe moest ik het volgende doen:

  1. Importeer de methods uit de DLL
  2. Maak een connectie met de hardware (een methodcall)
  3. Registreer welke gestures je wilt ontvangen (zoom, rotate, scroll, etc)
  4. Registreer een Windows Message die verstuurd wordt als er een touch wordt waargenomen
  5. Vang die message in je MessageLoop af
  6. Vertaal de wParam en lParam in de juiste types (een enum en een struct)
  7. Doe iets met die info in je applicatie

Er zijn 4 methods in de DLL die ik moet gebruiken. Deze zien er als volgt uit (in c++):

void* NtrGesturesConnect();
void NtrGesturesDisconnect(void* Handle);
bool NtrGesturesRegister(void* Handle, TNtrGestures* data, HWND hWnd);
void NtrGesturesUnRegister();

De TNtrGestures is een struct met daarin alle gestures waarin je geinteresseerd bent. Die ziet er als volgt uit (weer c++):

struct TNtrGestures
{
    bool ReceiveZoom;
    bool UseUserZoomSettings;
    bool ReceiveScroll;
    bool UseUserScrollSettings;
    bool ReceiveFingersDoubleTab;
    bool UseUserFingersDoubleTabSettings;
    bool ReceiveRotate;
    bool UseUserRotateSettings;
}

Niet zo moeilijk, nietwaar? Je geeft aan welke gestures je wilt krijgen door de juiste velden op true te zetten (je kunt ook aangeven of je de raw data wilt of dat de data moet worden aangepast aan de voorkeuren van de gebruiker) en that's it.

Importeren van de functies

Dit moest even vertaald worden in C#. Geen probleem, doen we even!

class Digitizer
{
    [DllImport(@".\NtrigISV.dll")]
    public static extern IntPtr NtrGesturesConnect();
 
    [DllImport(@".\NtrigISV.dll")]
    public static extern void NtrGestureDisconnect(IntPtr handle);
 
    [DllImport(@".\NtrigISV.dll")]
    public static extern bool NtrGesturesRegister(IntPtr handle, TNtrGestures data, IntPtr hWnd);
 
    [DllImport(@".\NtrigISV.dll")]
    public static extern IntPtr NtrGesturesUnRegister();
 
    [DllImport(@"user32.dll")]
    public static extern uint RegisterWindowMessage(string lpString);
 
}

Een class gemaakt en daarin de methods gezet. Ok. Laten we eens gaan testen. Een WPF applicatie gemaakt en eerst een kijken of we kunnen connecten met de digitizer.

Helaas. Dat gaat dus niet. De eerste foutmelding was een feit.

Error1

De DLL kon hij wel vinden, maar de method was niet beschikbaar. Ik heb 10 keer gekeken of ik geen typfout gemaakt had, maar dit is precies hoe het in de documentatie stond. Misschien was de documentatie fout? Was de naam toch anders in mijn versie van de DLL? De VS CommandLineTool DumpBin /exports NtrigISV.dll bracht uitkomt:

image

De method heet niet NtrGesturesConnect, maar die heet ?NtrGesturesConnect@@YAPAXXZ. Stom van me. Natuurlijk. Uhm.. WTF?

Oh ja. Een ontwikkelaar die een DLL maakt met functies die geexporteerd gaan worden, moet dat wel even netjes aangeven met _declspec(dllexport) tijdens compileren: de C++ compiler genereert voor iedere functie een unieke naam om er voor te zorgen dat overloading blijft werken. Maar dat wil je niet bij geexporteerde functies. de _declspec(dllexport) zorgt ervoor dat de naam niet wijzigt. En dat is precies wat ik nodig had.

Nou ja, geen probleem: je kunt functies ook uit een DLL halen op basis van hun volgnummer. De ordinals staan er keurig bij. De code wordt nu dus:

class Digitizer
{
    [DllImport(@".\NtrigISV.dll", EntryPoint="#1")]
    public static extern IntPtr NtrGesturesConnect();
 
    [DllImport(@".\NtrigISV.dll", EntryPoint="#2")]
    public static extern void NtrGestureDisconnect(IntPtr handle);
 
    [DllImport(@".\NtrigISV.dll", EntryPoint="#3")]
    public static extern bool NtrGesturesRegister(IntPtr handle, TNtrGestures data, IntPtr hWnd);
 
    [DllImport(@".\NtrigISV.dll", EntryPoint="#4")]
    public static extern IntPtr NtrGesturesUnRegister();
 
    [DllImport(@"user32.dll"]
    public static extern uint RegisterWindowMessage(string lpString);
}

De EntryPoint geeft aan hoe de functie in de DLL heet. Je kunt dus in je C# code andere namen gebruiken dan de schrijver van de DLL bedacht heeft. Ook kun je door een # te gebruiken de ordinal aangeven: feitelijk zeg ik hier: haal functie 1 op en noem hem NtrGesturesConnect. En dat werkte prima.

Ik kan connecten! Het werkt als een trein! Nou ja, ik krijg geen foutmelding en dat is al heel wat! Stap 1 en 2 zijn gedaan (importeren en connecten), stap 3 is simpel (maak een instance van je struct aan, zet de juiste waardes op true). Nu stap 4. Registreer een Windowsmessage.

De messageloop uitgelegd

Ok. Als je niet weet wat Windows Messages zijn volgt hier een korte uitleg. Weet je het wel, sla dit dan gerust over.

Windows werkt als volgt: het Operating System, de drivers en alle applicaties sturen elkaar voortdurend berichten. Ieder window kan een dergelijk bericht ontvangen en versturen. Events bestaan niet in Windows, het zijn allemaal messages. Iedere message heeft een nummer, ieder window (dat zijn dus alle windows op je scherm, maar ook alle dialogs en vrijwel alle controls ook: ja, ook een button is een window), heeft een unieke identifier, hWnd genaamd (oftewel Handle to WiNDow => hWnd).

In je applicatie maak je een messageloop, die niets anders doet als de volgende pseudocode:

Message = GetMessage();
while( Message != WM_QUIT )
{
  // Process message
}

Als windows vindt dat een applicatie moet stoppen, stuurt deze de WM_QUIT message naar je window en dan stopt de messageloop en ook je applicatie. Een Window kan ook alle messages voor zijn childwindows krijgen (de de window kan de message krijgen die aangeeft dat er op een button geklikt wordt).

Feitelijk is dit wat er gebeurd in je Application.Run() aanroep in je main van je .net applicatie.

Goed, dit is wel een enorme versimpeling van hoe het allemaal onder water werkt, maar voor nu is het genoeg.

De messageloop in een WPF applicatie

Hoe vangen we nu een message af in WPF? Onder water is ieder WPF form gewoon een window, dus er zit een messageloop ergens. Maar we kunnen er niet bij... Voor dit soort gevallen kent Windows het concept MessageHooks. Dit zijn methods die aangeroepen worden vanuit de messageloop en zorgen ervoor dat de messages ook terecht komen op de plek waar je er wel bij kunt.

In de Window_Loaded event handler van je WPF form plaats je de volgende code:

HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
source.AddHook(new HwndSourceHook(this.WndProc));

Met behulp van de WindowInterHelper class haal je de Handle van dit window op, met HwndSource.FromHwnd maak je een wrapper om de standaard windows window aan. Vervolgens roep je AddHook aan met een HwnSourceHook die de hook plaatst. De this.WndProc is mijn method die aangeroepen wordt als je een message ontvangt.

public IntPtr WndProc(
    IntPtr hwnd, 
    int msg, 
    IntPtr wParam, 
    IntPtr lParam, 
    ref bool handled)
{
    // Doe iets met de message...
    // als je hem afhandelt en niet
    // wilt dat Windows er nog iets mee
    // doet, zet je handled op true.
    return IntPtr.Zero;
}

Probeer dit eens en zet eens een breakpoint op deze method. Scrhik niet van de hoeveelheid messages die je ontvangt.... honderden per seconde is geen uitzondering. Uiteraard zijn we niet geinteresseerd in alle messages, alleen in de message die aangeeft dat er een multi-touch ding gebeurt is. Welke message is dat? Tja... die is niet bekend binnen Windows, tenminste niet tot we Windows 7 hebben.

Door de Windows API RegisterWindowMessage aan te roepen kunnen we die message krijgen. Deze functie krijgt een string mee. Als deze functie voor het eerst sinds Windows is opgestart is wordt aangeroepen, zoekt Windows een nummer op die nog niet gebruikt is en geeft die terug. Als het al eerder aangeroepen is met deze string, krijg je de al eerder gegenereerde message id (want dat is het) terug. Het maakt dus niet uit wie als eerste RegisterWindowMessage aanroept, als iedereen dat maar met dezelfde string doet. Komt 'ie:

_NTrigMessage = Digitizer.RegisterWindowMessage("NtrOnGestureWindowsMessage");

We krijgen nu een Message Id terug, die de DLL zal gebruiken om ons te informeren over de gestures die gedaan worden. We moeten in onze messagehook (this.WindProc) deze even afvangen door een if(msg == _NTrigMessage).... te doen.

wParam en lParam casten

Op het moment dat er een message binnen komt, staat in de wParam wat er gebeurt is. De SDK defnieert een enum, die 1 op 1 over te zetten is naar C#:

public enum ENtrGestureType
{
    ScrollV,
    ScrollH,
    Zoom,
    Rotate,
    DoubleTap
}

We kunnen de wParam casten naar een ENtrGestureType en dan zien we wat er gebeurd is: was er een zoom, een rotate, iets anders? Maar we hebben daar niet genoeg aan. Als we een zoom binnenkrijgen, dan willen we ook weten hoeveel zoom er dan is. Is het inzoomen? Uitzoomen? Hoeveel? Die informatie wordt door de DLL in een struct teruggegeven:

public struct TntrGestureZoom
{
    public double mAmount;
    public ushort X;
    public ushort Y;
    public ushort Width;
    public ushort Height;
}

Rotate, ScrollH, ScrollV en DoubleTap hebben vergelijkbare structs met de relevante informatie.

Maar hoe krijgen we die nou in onze applicatie? De voorbeeld code is simpel: ze casten de lParam naar een TntrGestureZoom*. Een pointer dus. Maar hoe doen we dan in C#? Op zich is het niet zo moeilijk: lParam is immers al een IntPtr oftewel een pointer. Maar die kun je niet eenvoudig omzetten naar iets wat wij kunnen gebruiken. De oplossing ligt in het gebruik van de Marshal class. Deze class bevat een aantal helpers die het werken met COM en P/Invoke vergemakkelijkt. Je moet even zoeken, maar dan vind je uiteindelijk de volgende oplossing.

TntrGestureZoom data =(TntrGestureZoom) Marshal.PtrToStructure(tntrGestureZoom, typeof(TntrGestureZoom));

Marshal.PtrToStructure maakt van de pointer naar de structure een echte C# structure van het juiste type. En nu kunnen we data uitlezen en kijken wat er gebeurdt.

Vol spanning start ik de applicatie, doe een zoom met mijn vingers en....

Ja! Het werkt! Ik krijg data door dat ik in of uitzoom, en hoeveel! Ik kan er iets mee! Ok, nu even roteren doen. Ik doe hetzelfde trucje met de Marshal.PtrToStructure maar nu voor de rotate struct, en probeer het weer. En weer. En nog een keer. En wat ik ook doe: geen enkel effect. Alleen de zoom werkt. Geen rotate, geen Scroll, geen DoubleTab.

Zou mijn computer stuk zijn? Ik heb even de standaard HP software opgestart maar die werkt prima. Oh wacht even: werkt die wel op dezelfde manier? Even met Spy++ gekeken welke messages er naar de applicatie gestuurd worden en inderdaad: de officiele HP software roept niet dezelfde DLL aan. Zucht. Is de DLL misschien niet goed?

Ik heb uiteindelijk mijn applicatie herschreven in C++. En wat schetst mijn verbazing? Het werkt! Rotate komt nu ook binnen! Het goede nieuws is dus dat mijn hardware goed is en dat de DLL zijn werk doet. Alleen in mijn C# applicatie doet het niet goed.

Wees gerust: ik heb het opgelost. Het kostte me veel moeite, maar uiteindelijk kreeg ik het voor elkaar.

Structs en booleans

Ik vond het verdacht dat alleen Zoom het deed: dat is precies de eerste boolean waarde in de struct die aangeeft welke gestures ik wilde ontvangen. Zou de struct niet goed doorgegeven worden aan de DLL? Ik weet dat structs die naar dll gestuurd worden een bepaalde layout in het geheugen nodig hebben (alle velden staan keurig achter elkaar). Nou kan je dat expliciet aangeven dus dat heb ik maar even gedaan (hoewel het volgens MSDN niet nodig is!)

[StructLayout(LayoutKind.Sequential)]
struct TNtrGestures{   
    bool ReceiveZoom;    
    bool UseUserZoomSettings;    
    bool ReceiveScroll;    
    bool UseUserScrollSettings;    
    bool ReceiveFingersDoubleTab;    
    bool UseUserFingersDoubleTabSettings;    
    bool ReceiveRotate;    
    bool UseUserRotateSettings;
}

Ik geef hier aan dat ik alle velden sequentieel in het geheugen op wil slaan. Maar helaas. Ook dit hielp niet. Als ik ReceiveZoom op false zette en de rest op true, kreeg ik helemaal niets meer binnen. De struct werd dus wel doorgegeven. Maar wat is het dan?

En toen wist ik het. Wat is een boolean eigenlijk? Hoe wordt dat doorgegeven? Na lang spitten kwam ik er achter dat een C# bool (alias voor System.Boolean) wanneer hij aan een C++ dll gegeven wordt, gecast wordt naar een int. En een int is 4 bytes groot. Een bool in c/c++ is 1 byte groot.... Even proberen: ik maak in mijn struct geen gebruik meer van bools maar van bytes. Alles wat ik op true wil hebben, zet ik op 1 en de rest op 0. Zo werkt dat immers in c/c++. En jawel hoor. De rotates en de zooms vlogen me om de oren.

Toch was ik hier niet tevreden mee: het is immers geen byte in die struct, maar een bool! Ik wil in mijn WPF forms gebruik kunnen maken van boolean waardes en niet van bytes. Dus moet ik een wrapper class maken die de boel (woordgrapje, sorry) omzet?

Nee. Er is een attribuut die je mee kunt geven om aan te geven hoe types gemarshalled (oftewel omgezet) kunnen worden. Bijna alle artikelen daarover zeggen dat het bijna nooit nodig is, want C# is zelf zo slim om de types juist om te zetten. Maar in die ene enkele gevallen waar het niet goed gaat... Affijn: dit is de struct geworden:

public struct TNtrGestures
{
    [MarshalAs(UnmanagedType.I1)]
    public bool ReceiveZoom;
    [MarshalAs(UnmanagedType.I1)]
    public bool UseUserZoomSettings;
    [MarshalAs(UnmanagedType.I1)]
    public bool ReceiveScroll;
    [MarshalAs(UnmanagedType.I1)]
    public bool UseUserScrollSettings;
    [MarshalAs(UnmanagedType.I1)]
    public bool ReceiveFingersDoubleTap;
    [MarshalAs(UnmanagedType.I1)]
    public bool UseUserFingersDoubleTapSettings;
    [MarshalAs(UnmanagedType.I1)]
    public bool ReceiveRotate;
    [MarshalAs(UnmanagedType.I1)]
    public bool UseUserRotateSettings;
}

En nu werkt alles zoals het moet. Ik krijg je juiste messages binnen, ik kan de lParam casten en alles werkt.

Nu nog even uitzoeken hoe je een WPF ViewPort3D kan roteren en zoomen en  verschuiven zonder helemaal gek te worden van de Quaternion berekeningen. En nee: de standaard Rotate en Translate werkt niet.....

Published Friday, February 20, 2009 2:34 PM door dvroegop
Filed Under: ,

Comments

 

peSHIr said:

Cool zeg. Isn't interop peachy? ;-)

Jammer dat ik geen multitouch hardware heb, anders zou ik er misschien ook eens mee gaan spelen. Moet nu nog maar even wachten, ben ik bang...
March 26, 2009 3:47 PM
Anonymous comments are disabled

About dvroegop

Programmeert al sinds 1982. Is nu werkzaam als software architect bij Detrio Consultancy b.v.
Powered by Community Server, by Telligent Systems