XNA and Windows 7 Multi-touch

牛逼人总是有的。推荐下面的这篇文章,转载自:http://xna-uk.net/blogs/randomchaos/archive/2010/02/10/xna-and-windows-7-multi-touch.aspx

我只能景仰。

I am pretty sure, that like me you are more than capable of scanning the web for snippets of info on how to do this, well I thought, to save you the time and effort I’ll put up a simple how-to here.

As ever I am standing on the shoulder of giants, the first thing I do to get multi-touch into my XNA project is to use a regular windows form to render to. I dare say there it a much better way to do this with WPF, which I am sure I’ll have a look at when I have time, but for now,  I am using the good old 2.0 Windows Form.  Now for this I have used a great post by another XNA MVP Pedro Guida aka “Ultrahead” showing how to get XNA to render to a panel control. I can’t for the life of me find his original post, it may have even been on The Code Project and also used the MS Multitouch code samples.

The spawn of these two resources has  given me my own Panel control that XNA can render to AND the Windows 7 Multi-touch API can be hooked up to.

So, how does all this hang together first off we open up a new XNA Windows Project, then Add a Windows Form to the project, we then need to create our own Panel object so that it can interact with multi-touch.

The code for the new Panel class is 99% taken from the MS Multitouch samples and it involves hooking into the WndProc method on the control.

XNAWindowsMTPanel

     public  class  XNAWindowsMTPanel :  Panel
    {
         ///////////////////////////////////////////////////////////////////////
         // Protected members, for derived classes.

         // Touch event handlers
         public  event  EventHandler< WMTouchEventArgs> Touchdown;    // touch down event handler
         public  event  EventHandler< WMTouchEventArgs> Touchup;      // touch up event handler
         public  event  EventHandler< WMTouchEventArgs> TouchMove;    // touch move event handler

         // EventArgs passed to Touch handlers
         public  class  WMTouchEventArgs : System. EventArgs
        {
             // Private data members
             private  int x;                   // touch x client coordinate in pixels
             private  int y;                   // touch y client coordinate in pixels
             private  int id;                  // contact ID
             private  int mask;                // mask which fields in the structure are valid
             private  int flags;               // flags
             private  int time;                // touch event time
             private  int contactX;            // x size of the contact area in pixels
             private  int contactY;            // y size of the contact area in pixels

             // Access to data members
             public  int LocationX
            {
                 get {  return x; }
                 set { x =  value; }
            }
             public  int LocationY
            {
                 get {  return y; }
                 set { y =  value; }
            }
             public  int Id
            {
                 get {  return id; }
                 set { id =  value; }
            }
             public  int Flags
            {
                 get {  return flags; }
                 set { flags =  value; }
            }
             public  int Mask
            {
                 get {  return mask; }
                 set { mask =  value; }
            }
             public  int Time
            {
                 get {  return time; }
                 set { time =  value; }
            }
             public  int ContactX
            {
                 get {  return contactX; }
                 set { contactX =  value; }
            }
             public  int ContactY
            {
                 get {  return contactY; }
                 set { contactY =  value; }
            }
             public  bool IsPrimaryContact
            {
                 get {  return (flags & TOUCHEVENTF_PRIMARY) != 0; }
            }

             // Constructor
             public WMTouchEventArgs()
            {
            }
        }

         ///////////////////////////////////////////////////////////////////////
         // Private class definitions, structures, attributes and native fn's
         //Exercise1-Task2-Step2 

         // Touch event window message constants [winuser.h]
         private  const  int WM_TOUCHMOVE = 0x0240;
         private  const  int WM_TOUCHDOWN = 0x0241;
         private  const  int WM_TOUCHUP = 0x0242;

         private  const  int WM_MOUSEMOVE = 0x0200;
         private  const  int WM_LBUTTONDOWN = 0x0202;
         private  const  int WM_LBUTTONUP = 0x0208;
         private  const  int WM_MBUTTONDBLCLK = 0x0209;

         // Touch event flags ((TOUCHINPUT.dwFlags) [winuser.h]
         private  const  int TOUCHEVENTF_MOVE = 0x0001;
         private  const  int TOUCHEVENTF_DOWN = 0x0002;
         private  const  int TOUCHEVENTF_UP = 0x0004;
         private  const  int TOUCHEVENTF_INRANGE = 0x0008;
         private  const  int TOUCHEVENTF_PRIMARY = 0x0010;
         private  const  int TOUCHEVENTF_NOCOALESCE = 0x0020;
         private  const  int TOUCHEVENTF_PEN = 0x0040;

         // Touch input mask values (TOUCHINPUT.dwMask) [winuser.h]
         private  const  int TOUCHINPUTMASKF_TIMEFROMSYSTEM = 0x0001;  // the dwTime field contains a system generated value
         private  const  int TOUCHINPUTMASKF_EXTRAINFO = 0x0002;  // the dwExtraInfo field is valid
         private  const  int TOUCHINPUTMASKF_CONTACTAREA = 0x0004;  // the cxContact and cyContact fields are valid

         // Touch API defined structures [winuser.h]
         //Exercise1-Task2-Step4 
        [ StructLayout( LayoutKind.Sequential)]
         private  struct  TOUCHINPUT
        {
             public  int x;
             public  int y;
             public System. IntPtr hSource;
             public  int dwID;
             public  int dwFlags;
             public  int dwMask;
             public  int dwTime;
             public System. IntPtr dwExtraInfo;
             public  int cxContact;
             public  int cyContact;
        }

        [ StructLayout( LayoutKind.Sequential)]
         private  struct  POINTS
        {
             public  short x;
             public  short y;
        }

         // Currently touch/multitouch access is done through unmanaged code
         // We must p/invoke into user32 [winuser.h]
         //Exercise1-Task2-Step3 
        [ DllImport( "user32")]
        [ returnMarshalAs( UnmanagedType.Bool)]
         private  static  extern  bool RegisterTouchWindow(System. IntPtr hWnd,  ulong ulFlags);

        [ DllImport( "user32")]
        [ returnMarshalAs( UnmanagedType.Bool)]
         private  static  extern  bool GetTouchInputInfo(System. IntPtr hTouchInput,  int cInputs, [ InOutTOUCHINPUT[] pInputs,  int cbSize);

        [ DllImport( "user32")]
        [ returnMarshalAs( UnmanagedType.Bool)]
         private  static  extern  void CloseTouchInputHandle(System. IntPtr lParam);

         // Attributes
         private  int touchInputSize;         // size of TOUCHINPUT structure

        [ SecurityPermission( SecurityAction.Demand)]
         public XNAWindowsMTPanel()
        {
            Dock =  DockStyle.Fill;
             // GetTouchInputInfo need to be
             // passed the size of the structure it will be filling
             // we get the sizes upfront so they can be used later.
            touchInputSize =  Marshal.SizeOf( new  TOUCHINPUT());
        }

         protected  override  CreateParams CreateParams
        {
             get
            {
                 CreateParams createParams =  base.CreateParams;
                createParams.ExStyle |= 0x00000020;  // WS_EX_TRANSPARENT
                 return createParams;
            }
        }

         protected  override  void CreateHandle()
        {
             base.CreateHandle();
             ulong ulFlags = 0;
            RegisterTouchWindow( this.Handle, ulFlags);
        }

         ///////////////////////////////////////////////////////////////////////
         // Private methods

         // Window procedure. Receives WM_ messages.
         // Translates WM_TOUCH window messages to touch events.
         // Normally, touch events are sufficient for a derived class,
         // but the window procedure can be overriden, if needed.
         // in:
         //      m       message
        [ PermissionSet( SecurityAction.Demand, Name =  "FullTrust")]
         protected  override  void WndProc( ref  Message m)
        {
             // Decode and handle WM_TOUCH* message.
             bool handled;
             switch (m.Msg)
            {
                 case WM_TOUCHDOWN:
                 case WM_TOUCHMOVE:
                 case WM_TOUCHUP:
                    handled = DecodeTouch( ref m);
                     break;
                 default:
                    handled =  false;
                     break;
            }

             // Call parent WndProc for default message processing.
             base.WndProc( ref m);

             if (handled)
            {
                 // Acknowledge event if handled.
                 try
                {
                    m.Result =  new System. IntPtr(1);
                }
                 catch ( Exception exception)
                {
                     Debug.Print( "ERROR: Could not allocate result ptr");
                     Debug.Print(exception.ToString());
                }
            }
        }

         // Extracts lower 16-bit word from an 32-bit int.
         // in:
         //      number      int
         // returns:
         //      lower word
         private  static  int LoWord( int number)
        {
             return number & 0xffff;
        }

         // Decodes and handles WM_TOUCH* messages.
         // Unpacks message arguments and invokes appropriate touch events.
         // in:
         //      m           window message
         // returns:
         //      flag whether the message has been handled
         private  bool DecodeTouch( ref  Message m)
        {
             // More than one touchinput may be associated with a touch message,
             // so an array is needed to get all event information.
             int inputCount = LoWord(m.WParam.ToInt32());  // Number of touch inputs, actual per-contact messages

             TOUCHINPUT[] inputs;  // Array of TOUCHINPUT structures
             try
            {
                inputs =  new  TOUCHINPUT[inputCount];  // Allocate the storage for the parameters of the per-contact messages
            }
             catch ( Exception exception)
            {
                 Debug.Print( "ERROR: Could not allocate inputs array");
                 Debug.Print(exception.ToString());
                 return  false;
            }

             // Unpack message parameters into the array of TOUCHINPUT structures, each
             // representing a message for one single contact.
             //Exercise2-Task1-Step3 
             if (!GetTouchInputInfo(m.LParam, inputCount, inputs, touchInputSize))
            {
                 // Get touch info failed.
                 return  false;
            }

             // For each contact, dispatch the message to the appropriate message
             // handler.
             // Note that for WM_TOUCHDOWN you can get down & move notifications
             // and for WM_TOUCHUP you can get up & move notifications
             // WM_TOUCHMOVE will only contain move notifications
             // and up & down notifications will never come in the same message
             bool handled =  false// // Flag, is message handled
             //Exercise2-Task1-Step4
             for ( int i = 0; i < inputCount; i++)
            {
                 TOUCHINPUT ti = inputs[i];

                 // Assign a handler to this message.
                 EventHandler< WMTouchEventArgs> handler =  null;      // Touch event handler
                 if ((ti.dwFlags & TOUCHEVENTF_DOWN) != 0)
                {
                    handler = Touchdown;
                }
                 else  if ((ti.dwFlags & TOUCHEVENTF_UP) != 0)
                {
                    handler = Touchup;
                }
                 else  if ((ti.dwFlags & TOUCHEVENTF_MOVE) != 0)
                {
                    handler = TouchMove;
                }

                 // Convert message parameters into touch event arguments and handle the event.
                 if (handler !=  null)
                {
                     // Convert the raw touchinput message into a touchevent.
                     WMTouchEventArgs te;  // Touch event arguments

                     try
                    {
                        te =  new  WMTouchEventArgs();
                    }
                     catch ( Exception excep)
                    {
                         Debug.Print( "Could not allocate WMTouchEventArgs");
                         Debug.Print(excep.ToString());
                         continue;
                    }

                     // TOUCHINFO point coordinates and contact size is in 1/100 of a pixel; convert it to pixels.
                     // Also convert screen to client coordinates.
                    te.ContactY = ti.cyContact / 100;
                    te.ContactX = ti.cxContact / 100;
                    te.Id = ti.dwID;
                    {
                         Point pt = PointToClient( new  Point(ti.x / 100, ti.y / 100));
                        te.LocationX = pt.X;
                        te.LocationY = pt.Y;
                    }
                    te.Time = ti.dwTime;
                    te.Mask = ti.dwMask;
                    te.Flags = ti.dwFlags;

                     // Invoke the event handler.
                    handler( this, te);

                     // Mark this event as handled.
                    handled =  true;
                }
            }

            CloseTouchInputHandle(m.LParam);

             return handled;
        }
    }

So now we have a control we can use to get MT input for, we now need to hook this control up to our form and in turn to our XNA Game class. In the form we need to give our Game class access to the handle it is going to render to. To do this we define XNAWindowsMTPanel object, create a Property on the form so that it can be accessed and then instantiate the XNAWindowsMTPanel object. This leaves our form source code looking like this:

     public  partial  class  frmMain :  Form
    {
         public  XNAWindowsMTPanel panel1;

         public  IntPtr DisplayHandle
        {
             get
            {
                 return  this.panel1.IsHandleCreated ?
                        this.panel1.Handle :  IntPtr.Zero;
            }
        }

         public frmMain( Game1 game)
        {
            InitializeComponent();

            panel1 =  new  XNAWindowsMTPanel();

            

             this.Controls.Add(panel1);
        }
        
    }

Now in our game class we need to set up the Windows form to be the form we will be rendering to, we create a From object, in the constructor, instantiate, bind to the forms HandleDestroyed event so when the form closes we can tidy up and then show the form. We also need to be able to ‘hide’ our XNA game window to so to do this we need to bind to the XNA game windows Shown event and then hide the window. To help us do this we will need to reference System.Windows.Forms in our Game1 class and to stop any name space conflicts we will alias it with ‘SysWinForms’. We also need to set our view port so that it matches the area we are rendering to and this also needs to be done if the form is resized.

The top of our Game1.cs file now looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using SysWinForms = System.Windows.Forms;

namespace GenericXNAMultiTouch
{
     ///   <summary>
     ///  This is the main type for your game
     ///   </summary>
     public  class  Game1 : Microsoft.Xna.Framework. Game
    {
         GraphicsDeviceManager graphics;
         SpriteBatch spriteBatch;

         frmMain frmMain;

        

         public Game1()
        {
            graphics =  new  GraphicsDeviceManager( this);
            Content.RootDirectory =  "Content";

            SysWinForms. Form gameWindow = (SysWinForms. Form)SysWinForms. Form.FromHandle( this.Window.Handle);
            gameWindow.Shown +=  new  EventHandler(gameWindow_Shown);

            frmMain =  new  frmMain( this);
            frmMain.HandleDestroyed +=  new  EventHandler(frmMain_HandleDestroyed);
            frmMain.Resize +=  new  EventHandler(frmMain_Resize);
            frmMain.Show();

            graphics.PreferredBackBufferHeight = frmMain.panel1.Height;
            graphics.PreferredBackBufferWidth = frmMain.panel1.Width;
        }

         void frmMain_Resize( object sender,  EventArgs e)
        {
             if (GraphicsDevice.Viewport.Width != frmMain.panel1.Width || GraphicsDevice.Viewport.Height != frmMain.panel1.Height)
            {
                GraphicsDevice.Viewport =  new  Viewport
                {
                    X = 0,
                    Y = 0,
                    Height = frmMain.panel1.Height,
                    Width = frmMain.panel1.Width,
                    MaxDepth = GraphicsDevice.Viewport.MaxDepth,
                    MinDepth = GraphicsDevice.Viewport.MinDepth,
                };
            }
        }

         void frmMain_HandleDestroyed( object sender,  EventArgs e)
        {
            Exit();
        }

         void gameWindow_Shown( object sender,  EventArgs e)
        {
            ((SysWinForms. Form)sender).Hide();
        }

Running this now will just give us a blank Windows Form, we need to now tell our Game1 class to render to our XNAWindowsMTPanel and this is very simple to do, simply at the end of the Draw call do this:

            GraphicsDevice.Present(frmMain.DisplayHandle);

You will now have your XNA code rendering on the windows form!! How cool is that!

Now to add the MT functionality. You can see on the XNAWindowsMTPanel class that we have some events we can bind to:

         // Touch event handlers
         public  event  EventHandler< WMTouchEventArgs> Touchdown;    // touch down event handler
         public  event  EventHandler< WMTouchEventArgs> Touchup;      // touch up event handler
         public  event  EventHandler< WMTouchEventArgs> TouchMove;    // touch move event handler

So we we will get some code stubs in our Game1 class then wire these up in Windows Form (again taken from the MS MT samples)

        #region Multi-Touch
         // Touch Stuff
         // Touch down event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchDownHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
            
        }

         // Touch up event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchUpHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
            
        }

         // Touch move event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchMoveHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
            
        }
        #endregion

And now to wire them up in the form:

         public frmMain( Game1 game)
        {
            InitializeComponent();

            panel1 =  new  XNAWindowsMTPanel();

             // Touch stuff
             // Setup handlers
            panel1.Touchdown += game.OnTouchDownHandler;
            panel1.Touchup += game.OnTouchUpHandler;
            panel1.TouchMove += game.OnTouchMoveHandler;

             this.Controls.Add(panel1);
        }

So, we now have our XNA project rendering on a windows form that is also able to accept munti touch commands, so how can we demonstrate this?

First of all we are going to create an object that we can use to store the touch events in, this will also hold some data to help us represent the touch on the screen.

     public  class  MTObject
    {
         public  XNAWindowsMTPanel. WMTouchEventArgs Event;
         public  Point[] spritePositions;
         public  Color Color;
         public  float Rotation = 0;
         public  int Size;

         public MTObject( XNAWindowsMTPanel. WMTouchEventArgs e, int size, Color color)
        {
            Event = e;
            spritePositions =  new  Point[100];
            Color = color;
            Size = size;
        }        
    }

We then create a Dictionary to store these items in, I am also going to add a Texture2D to render the touch objects and give an audio que with a SoundEffect Object. This is hoe I have defined them in the Game1 class:

         Dictionary< intMTObject> touchList =  new  Dictionary< intMTObject>();

         Texture2D sprite;
         SoundEffect touchWav;

 

 

 

 

Now all we need to do is record when we have a touch contact, when one is removed and when one is moved. Our touch event handlers now look like this:

        #region Multi-Touch
         // Touch Stuff
         // Touch down event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchDownHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
             // Is this a new touch or an old one?
             if (!touchList.Keys.Contains(e.Id))
            {
                 if (e.IsPrimaryContact)
                    touchList.Add(e.Id,  new  MTObject(e, 32, Color.Red));
                 else
                    touchList.Add(e.Id,  new  MTObject(e, 16,  Color.Gold));
            }

            touchWav.Play();

        }

         // Touch up event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchUpHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
             if (touchList.Keys.Contains(e.Id))
                touchList.Remove(e.Id);
        }

         // Touch move event handler.
         // in:
         //      sender      object that has sent the event
         //      e           touch event arguments
         public  void OnTouchMoveHandler( object sender,  XNAWindowsMTPanel. WMTouchEventArgs e)
        {
            touchList[e.Id].Event.LocationX = e.LocationX;
            touchList[e.Id].Event.LocationY = e.LocationY;
        }
        #endregion

I then make sure that the array of points I am using to represent the touch contacts is updated, effectively giving a string of particles after the touch contact point, giving us the following Update and Draw methods:

         ///   <summary>
         ///  Allows the game to run logic such as updating the world,
         ///  checking for collisions, gathering input, and playing audio.
         ///   </summary>
         ///   <param name="gameTime"> Provides a snapshot of timing values. </param>
         protected  override  void Update( GameTime gameTime)
        {
             // Allows the game to exit
             if ( GamePad.GetState( PlayerIndex.One).Buttons.Back ==  ButtonState.Pressed)
                 this.Exit();

             foreach ( int key  in touchList.Keys)
            {
                 for ( int s = touchList[key].spritePositions.Length - 1; s > 0; s--)
                {
                    touchList[key].spritePositions[s] = touchList[key].spritePositions[s - 1];
                }
                touchList[key].spritePositions[0] =  new  Point(touchList[key].Event.LocationX, touchList[key].Event.LocationY);
            }

             base.Update(gameTime);
        }

         ///   <summary>
         ///  This is called when the game should draw itself.
         ///   </summary>
         ///   <param name="gameTime"> Provides a snapshot of timing values. </param>
         protected  override  void Draw( GameTime gameTime)
        {
            GraphicsDevice.Clear( Color.CornflowerBlue);

             base.Draw(gameTime);

             Color alphaColor;

            spriteBatch.Begin();
             foreach ( int key  in touchList.Keys)
            {
                 for ( int s = touchList[key].spritePositions.Length - 1; s > 0; s--)
                {
                    touchList[key].spritePositions[s] = touchList[key].spritePositions[s - 1];
                    alphaColor =  new  Color(touchList[key].Color.R, touchList[key].Color.G, touchList[key].Color.B, (1 - (s / ( float)touchList[key].spritePositions.Length)));
                    spriteBatch.Draw(sprite,  new  Rectangle(touchList[key].spritePositions[s].X, touchList[key].spritePositions[s].Y, touchList[key].Size, touchList[key].Size),  new  Rectangle(0, 0, sprite.Width, sprite.Height), alphaColor, touchList[key].Rotation++,  new  Vector2(sprite.Width / 2, sprite.Height / 2),  SpriteEffects.None, 1);
                }
                
                alphaColor = touchList[key].Color;
                spriteBatch.Draw(sprite,  new  Rectangle(touchList[key].spritePositions[0].X, touchList[key].spritePositions[0].Y, touchList[key].Size, touchList[key].Size),  new  Rectangle(0, 0, sprite.Width, sprite.Height), alphaColor, touchList[key].Rotation++,  new  Vector2(sprite.Width / 2, sprite.Height / 2),  SpriteEffects.None, 1);
                

            }
            spriteBatch.End();

            GraphicsDevice.Present(frmMain.DisplayHandle);
        }

  

And that’s about it, hope you find it useful. The solution for this post can be found here. As ever C&C welcome.


你可能感兴趣的:(XNA and Windows 7 Multi-touch)