This article documents my notes on reading the WPF source code, and how the underlying WPF message handling transforms the WM_POINTER message obtained from the Win32 message loop into a parameter for the Touch event.
Since the WPF touch part will take into account the open Pointer message and not open Pointer message, in order to facilitate your understanding, this article is divided into two parts. The first part is to detach from the WPF framework, and talk about how a Win32 program can get the WM_POINTER message from the Win32 message loop and convert it to the input coordinate point, as well as get the touch information under the touch. The second part is how the WPF framework is arranged on these processing logic, and how to interface with the WPF framework.
Handling Pointer Messages
There are about three ways to handle Pointer messages in Win32 applications. I'll walk you through them from simple to complex.
Mode 1.
After receiving a WM_POINTER message, convert the wparam to apointerId
parameter, call the GetPointerTouchInfo method to get thePOINTER_INFO text
gainPOINTER_INFO (used form a nominal expression)ptPixelLocationRaw
field to get the pixel points based on the screen's coordinate system.
Simply convert it to a window coordinate system and process the DPI to use it.
The biggest disadvantage of this method is thatptPixelLocationRaw
The field gets the point where the precision is lost, in pixels. There will be a noticeable jagged effect if you are on a slightly higher precision touch screen
The advantage is that it is particularly easy to obtain
Way 2:
After receiving the WM_POINTER message, the wparam is converted to apointerId
parameter, call the GetPointerTouchInfo method to get thePOINTER_INFO text
Just from gettingPOINTER_INFO (used form a nominal expression)ptPixelLocationRaw
fields are replaced withptHimetricLocationRaw
field
utilizationptHimetricLocationRaw
field has the advantage of obtaining information without loss of precision, but requires an additional call to theGetPointerDeviceRects function to obtaindisplayRect
cap (a poem)pointerDeviceRect
Information is used to convert coordinate points
(, &pointerDeviceRect, &displayRect);
// If you want to get a high precision touch point, you can use the ptHimetricLocationRaw field.
// Since ptHimetricLocationRaw uses the pointerDeviceRect coordinate system, it needs to be converted to the screen coordinate system.
// The conversion is done by compressing the x-coordinate of ptHimetricLocationRaw into the [0-1] range, multiplying by the width of the displayRect, and adding the left value of the displayRect to get the x-coordinate of the screen coordinate system. Compressing to [0-1] is done by dividing by the width of pointerDeviceRect.
// Why do I need to add the value? Consider a multi-screen situation, where the screen may be a secondary screen.
// Y-coordinate is the same
var point2D = new Point2D(
/ (double) * +
, var point2D = new Point2D( / (double) * + ,
/ (double) * +
); var point2D = new Point2D( / (double)
// Get the points in the screen coordinate system, which need to be converted to the WPF coordinate system.
// There are two key points in the conversion process:
// 1. The underlying ClientToScreen only supports integer types, and direct conversion loses precision. Even WPF wrapped PointFromScreen or PointToScreen methods will lose precision.
// 2. DPI conversion is required, and must be DPI-aware.
// Measure the offset of the window from the screen, which is 0 0 points, because you're getting a virtual screen coordinate system and don't need to think about multiple screens
var screenTranslate = new Point(0, 0);
(new HWND(hwnd), ref screenTranslate);
// Get the current DPI value
var dpi = (this);
// Do the translation first, then do the DPI conversion.
point2D = new Point2D( - , - ); // get the current DPI value var dpi = (this); // do the translation first, then do the DPI conversion
point2D = new Point2D( / , / ); // do the translation first, then the DPI conversion.
The code for approach 2 above is placed in thegithub cap (a poem)gitee On, you can use the following command line to pull the code. My entire code repository is rather large, and a partial pull can be done faster using the following command line
First create an empty folder, then use the command line cd command to enter this empty folder, in the command line enter the following code, you can get the code of this article
git init
git remote add origin /lindexi/lindexi_gd.git
git pull origin 322313ee55d0eeaae7148b24ca279e1df087871e
The above uses the gitee source in China, if gitee is not accessible, please replace it with the github source. Please continue to enter the following code at the command line to replace the gitee source with the github source to pull the code. If you still can't pull the code, you can email me for the code.
git remote remove origin
git remote add origin /lindexi/lindexi_gd.git
git pull origin 322313ee55d0eeaae7148b24ca279e1df087871e
After getting the code, go to the WPFDemo/DefilireceHowemdalaqu folder to get the source code
The advantage of approach 2 is that higher accuracy can be obtained. The disadvantage is that it is relatively complex and requires more point processing
Way 3:
This approach is more complex, but can be more comprehensive, suitable for use in applications that require higher levels of control
first callGetPointerDeviceProperties method to obtain the corresponding device attributes reported by the HID descriptor, which at this point can be obtained with full HID descriptor attributes, may includePen Protocol for Windows The various attributes listed inside, such as width height rotation angle and other information
When a WM_POINTER message is received, call theGetRawPointerDeviceData Obtain the original touch information, and then parse the original touch information.
The parsing process of the original touch information needs to be applied first to get the packet length of each touch point, and then split the packet. The raw touch information is a binary array, this binary array may contain multiple touch point information, which needs to be split into multiple touch point information according to the packet length.
The parsing process means that, except for the first two data belonging to X and Y, respectively, the subsequent data are processed in accordance with theGetPointerDeviceProperties The touch description information obtained by the method is snapped into the
The complexity of this approach is higher and the raw touch information is obtained, which requires more processing. Even after parsing the X and Y coordinates, it is necessary to perform a coordinate transformation to the screen coordinate system.
The X and Y coordinate points you get here are the device coordinate system, which in this case is not theGetPointerDeviceRects function to get thepointerDeviceRect
device-wide coordinate system, but rather corresponds to theGetPointerDeviceProperties The range of coordinates of the logical maximum and minimum values of the descriptor obtained by the method
The correct way to calculate this is to start fromGetPointerDeviceProperties The X and Y descriptive information obtained by the method, respectively, takes thePOINTER_DEVICE_PROPERTY (used form a nominal expression)logicalMax
as the maximum value range. Divide X and Y bylogicalMax
zoom in[0,1]
range, then multiply by the screen size to convert to the screen coordinate system
The screen size here is determined by theGetPointerDeviceRects function to get thedisplayRect
sizes
After converting to the screen coordinate system, you need to process the DPI again and convert to the window coordinate system before you can use it.
You can see that way 3 is still relatively complex, but the advantage is that you can get more information about the device description and more information about the input point, such as the physical touch size area corresponding to the width of the touch can be calculated.
For the WPF framework, it is natural to go for the most complex and fully functional approach
Interfacing in the WPF framework
Now that we understand how a Win32 application interfaces with WM_POINTER messages, let's look at how WPF does it. Once you understand how to interface, the way to read the WPF source code is to find the whole WPF thread by referring to the methods that must be called.
Before we start, it must be noted that most of the code in this article is deleted code, only retained and the relevant part of this article. Now WPF is completely open source, based on the most friendly MIT protocol, you can pull down the code for the second modification of the release, you want to see the complete code and debug the whole process can be pulled from the open-source address from the entire warehouse down, the open-source address is:/dotnet/wpf
Inside WPF, the story of touch initialization starts with the Inside, call theGetPointerDevices method is initialized to get the number of devices, and each subsequent device calls theGetPointerDeviceProperties method to get the corresponding device attributes reported by the HID descriptor, with redacted code as follows
namespace
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // Ignore other codes
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = (ref deviceCount, deviceInfos);
... // Ignore other codes
}
}
}
Once the device is obtained, it is converted into a WPF-defined PointerTabletDevice, roughly as follows
namespace
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // Ignore other codes
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = (ref deviceCount, deviceInfos);
if (IsValid)
{
foreach (var deviceInfo in deviceInfos)
{
// Old PenIMC code gets this id via a straight cast from COM pointer address
// into an int32. This does a very similar thing semantically using the pointer
// to the tablet from the WM_POINTER stack. While it may have similar issues
// (chopping the upper bits, duplicate ids) we don't use this id internally
// and have never received complaints about this in the WISP stack.
int id = MS..IntPtrToInt32();
PointerTabletDeviceInfo ptdi = new PointerTabletDeviceInfo(id, deviceInfo);
// Don't add a device that fails initialization. This means we will try a refresh
// next time around if we receive stylus input and the device is not available.
// <see cref="">
if (())
{
PointerTabletDevice tablet = new PointerTabletDevice(ptdi);
_tabletDeviceMap[] = tablet;
();
}
}
}
... // Ignore other codes
}
/// <summary>
/// Holds a mapping of TabletDevices from their WM_POINTER device id
/// </summary>
private Dictionary<IntPtr, PointerTabletDevice> _tabletDeviceMap = new Dictionary<IntPtr, PointerTabletDevice>();
}
}
namespace
{
/// <summary>
/// Collection of the tablet devices that are available on the machine.
/// </summary>
public class TabletDeviceCollection : ICollection, IEnumerable
{
internal List<TabletDevice> TabletDevices { get; set; } = new List<TabletDevice>();
}
}
In PointerTabletDeviceInfo's TryInitialize method, theif (())
Inside this line of code, theGetPointerDeviceProperties The code logic to get the device attribute information is as follows
namespace
{
/// <summary>
/// WM_POINTER specific information about a TabletDevice
/// </summary>
internal class PointerTabletDeviceInfo : TabletDeviceInfo
{
internal PointerTabletDeviceInfo(int id, UnsafeNativeMethods.POINTER_DEVICE_INFO deviceInfo)
{
_deviceInfo = deviceInfo;
Id = id;
Name = _deviceInfo.productString;
PlugAndPlayId = _deviceInfo.productString;
}
internal bool TryInitialize()
{
... // Ignore other codes
var success = TryInitializeSupportedStylusPointProperties();
... // Ignore other codes
return success;
}
private bool TryInitializeSupportedStylusPointProperties()
{
bool success = false;
... // Ignore other codes
// Retrieve all properties from the WM_POINTER stack
success = (Device, ref propCount, null);
if (success)
{
success = (Device, ref propCount, SupportedPointerProperties);
if (success)
{
... // Perform more specific initialization logic
}
}
... // Ignore other codes
}
/// <summary>
/// The specific id for this TabletDevice
/// </summary>
internal IntPtr Device { get { return _deviceInfo.device; } }
/// <summary>
/// Store the WM_POINTER device information directly
/// </summary>
private UnsafeNativeMethods.POINTER_DEVICE_INFO _deviceInfo;
}
}
Why is the call toGetPointerDeviceProperties Twice? The first time is just to get the quantity, the second time is to really get the value
Reviewing the above code, you can see that the PointerTabletDeviceInfo object is created inside the Refresh method of the PointerTabletDeviceCollection, as shown in the following code
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // Ignore other codes
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = (ref deviceCount, deviceInfos);
foreach (var deviceInfo in deviceInfos)
{
// Old PenIMC code gets this id via a straight cast from COM pointer address
// into an int32. This does a very similar thing semantically using the pointer
// to the tablet from the WM_POINTER stack. While it may have similar issues
// (chopping the upper bits, duplicate ids) we don't use this id internally
// and have never received complaints about this in the WISP stack.
int id = MS..IntPtrToInt32();
PointerTabletDeviceInfo ptdi = new PointerTabletDeviceInfo(id, deviceInfo);
if (())
{
}
}
... // Ignore other codes
}
}
Get from GetPointerDevicesPOINTER_DEVICE_INFO
The information is stored in thePointerTabletDeviceInfo
(used form a nominal expression)_deviceInfo
Inside the field, as shown in the following code
internal class PointerTabletDeviceInfo : TabletDeviceInfo
{
internal PointerTabletDeviceInfo(int id, UnsafeNativeMethods.POINTER_DEVICE_INFO deviceInfo)
{
_deviceInfo = deviceInfo;
Id = id;
}
/// <summary>
/// The specific id for this TabletDevice
/// </summary>
internal IntPtr Device { get { return _deviceInfo.device; } }
/// <summary>
/// Store the WM_POINTER device information directly
/// </summary>
private UnsafeNativeMethods.POINTER_DEVICE_INFO _deviceInfo;
}
call (programming)GetPointerDeviceProperties When this is done, thePOINTER_DEVICE_INFO
(used form a nominal expression)device
field is passed in as a parameter to get thePOINTER_DEVICE_PROPERTY
Structure List Information
acquiredPOINTER_DEVICE_PROPERTY
The structure information corresponds very closely to the information reported by the HID descriptor. The code to define the structure is roughly as follows
/// <summary>
/// A struct representing the information for a particular pointer property.
/// These correspond to the raw data from WM_POINTER.
/// </summary>
[StructLayout(, CharSet = )]
internal struct POINTER_DEVICE_PROPERTY
{
internal Int32 logicalMin;
internal Int32 logicalMax;
internal Int32 physicalMin;
internal Int32 physicalMax;
internal UInt32 unit;
internal UInt32 unitExponent;
internal UInt16 usagePageId;
internal UInt16 usageId;
}
Based on HID basics, it is known that byusagePageId
cap (a poem)usageId
You can find out exactly what this device property means. See the HID standard documentation for more information:/developers/hidpage/Hut1_12v2.pdf
The Pointer used in WPF is theusagePageId
only the values listed in the following enumeration
/// <summary>
///
/// WM_POINTER stack must parse out HID spec usage pages
/// <see cref="/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
internal enum HidUsagePage
{
Undefined = 0x00,
Generic = 0x01,
Simulation = 0x02,
Vr = 0x03,
Sport = 0x04,
Game = 0x05,
Keyboard = 0x07,
Led = 0x08,
Button = 0x09,
Ordinal = 0x0a,
Telephony = 0x0b,
Consumer = 0x0c,
Digitizer = 0x0d,
Unicode = 0x10,
Alphanumeric = 0x14,
BarcodeScanner = 0x8C,
WeighingDevice = 0x8D,
MagneticStripeReader = 0x8E,
CameraControl = 0x90,
MicrosoftBluetoothHandsfree = 0xfff3,
}
The Pointer used in WPF is theusageId
only the values listed in the following enumeration
/// <summary>
///
///
/// WISP pre-parsed these, WM_POINTER stack must do it itself
///
/// See Stylus\ - 1
/// <see cref="/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
internal enum HidUsage
{
TipPressure = 0x30,
X = 0x30,
BarrelPressure = 0x31,
Y = 0x31,
Z = 0x32,
XTilt = 0x3D,
YTilt = 0x3E,
Azimuth = 0x3F,
Altitude = 0x40,
Twist = 0x41,
TipSwitch = 0x42,
SecondaryTipSwitch = 0x43,
BarrelSwitch = 0x44,
TouchConfidence = 0x47,
Width = 0x48,
Height = 0x49,
TransducerSerialNumber = 0x5B,
}
In older versions of WPF, the convention was to use GUIDs to get additional information about the data inside the StylusPointDescription. In order to be compatible with this behavior, HidUsagePage and HidUsage have been defined in WPF to correspond to GUIDs.
namespace
{
/// <summary>
/// StylusPointPropertyIds
/// </summary>
/// <ExternalAPI/>
internal static class StylusPointPropertyIds
{
/// <summary>
/// The x-coordinate in the tablet coordinate space.
/// </summary>
/// <ExternalAPI/>
public static readonly Guid X = new Guid(0x598A6A8F, 0x52C0, 0x4BA0, 0x93, 0xAF, 0xAF, 0x35, 0x74, 0x11, 0xA5, 0x61);
/// <summary>
/// The y-coordinate in the tablet coordinate space.
/// </summary>
/// <ExternalAPI/>
public static readonly Guid Y = new Guid(0xB53F9F75, 0x04E0, 0x4498, 0xA7, 0xEE, 0xC3, 0x0D, 0xBB, 0x5A, 0x90, 0x11);
public static readonly Guid Z = ...
...
/// <summary>
///
/// WM_POINTER stack usage preparation based on associations maintained from the legacy WISP based stack
/// </summary>
private static Dictionary<HidUsagePage, Dictionary<HidUsage, Guid>> _hidToGuidMap = new Dictionary<HidUsagePage, Dictionary<HidUsage, Guid>>()
{
{ ,
new Dictionary<HidUsage, Guid>()
{
{ , X },
{ , Y },
{ , Z },
}
},
{ ,
new Dictionary<HidUsage, Guid>()
{
{ , Width },
{ , Height },
{ , SystemTouch },
{ , NormalPressure },
{ , ButtonPressure },
{ , XTiltOrientation },
{ , YTiltOrientation },
{ , AzimuthOrientation },
{ , AltitudeOrientation },
{ , TwistOrientation },
{ , TipButton },
{ , SecondaryTipButton },
{ , BarrelButton },
{ , SerialNumber },
}
},
};
/// <summary>
/// Retrieves the GUID of the stylus property associated with the usage page and usage ids
/// within the HID specification.
/// </summary>
/// <param name="page">The usage page id of the HID specification</param>
/// <param name="usage">The usage id of the HID specification</param>
/// <returns>
/// If known, the GUID associated with the usagePageId and usageId.
/// If not known,
/// </returns>
internal static Guid GetKnownGuid(HidUsagePage page, HidUsage usage)
{
Guid result = ;
Dictionary<HidUsage, Guid> pageMap = null;
if (_hidToGuidMap.TryGetValue(page, out pageMap))
{
(usage, out result);
}
return result;
}
}
}
Through the above_hidToGuidMap
to call the GetKnownGuid method.POINTER_DEVICE_PROPERTY
Description of the information associated with the definition of the WPF framework layer
The specific correspondence logic is as follows
namespace
{
/// <summary>
/// Contains a WM_POINTER specific functions to parse out stylus property info
/// </summary>
internal class PointerStylusPointPropertyInfoHelper
{
/// <summary>
/// Creates WPF property infos from WM_POINTER device properties. This appropriately maps and converts HID spec
/// properties found in WM_POINTER to their WPF equivalents. This is based on code from the WISP implementation
/// that feeds the legacy WISP based stack.
/// </summary>
/// <param name="prop">The pointer property to convert</param>
/// <returns>The equivalent WPF property info</returns>
internal static StylusPointPropertyInfo CreatePropertyInfo(UnsafeNativeMethods.POINTER_DEVICE_PROPERTY prop)
{
StylusPointPropertyInfo result = null;
// Get the mapped GUID for the HID usages
Guid propGuid =
(
(),
());
if (propGuid != )
{
StylusPointProperty stylusProp = new StylusPointProperty(propGuid, (propGuid));
// Set Units
StylusPointPropertyUnit? unit = ();
// If the parsed unit is invalid, set the default
if (!)
{
unit = (stylusProp).Unit;
}
// Set to default resolution
float resolution = (stylusProp).Resolution;
short mappedExponent = 0;
if (_hidExponentMap.TryGetValue((byte)( & HidExponentMask), out mappedExponent))
{
float exponent = (float)(10, mappedExponent);
// Guard against divide by zero or negative resolution
if ( - > 0)
{
// Calculated resolution is a scaling factor from logical units into the physical space
// at the given exponentiation.
resolution =
( - ) / (( - ) * exponent);
}
}
result = new StylusPointPropertyInfo(
stylusProp,
,
,
,
resolution);
}
return result;
}
}
}
A minor point of detail in the above is the treatment of unit units, i.e.StylusPointPropertyUnit? unit = ();
The implementation definition of this line of code is implemented as follows
internal static class StylusPointPropertyUnitHelper
{
/// <summary>
/// Convert WM_POINTER units to WPF units
/// </summary>
/// <param name="pointerUnit"></param>
/// <returns></returns>
internal static StylusPointPropertyUnit? FromPointerUnit(uint pointerUnit)
{
StylusPointPropertyUnit unit = ;
_pointerUnitMap.TryGetValue(pointerUnit & UNIT_MASK, out unit);
return (IsDefined(unit)) ? unit : (StylusPointPropertyUnit?)null;
}
/// <summary>
/// Mapping for WM_POINTER based unit, taken from legacy WISP code
/// </summary>
private static Dictionary<uint, StylusPointPropertyUnit> _pointerUnitMap = new Dictionary<uint, StylusPointPropertyUnit>()
{
{ 1, },
{ 2, },
{ 3, },
{ 4, },
};
/// <summary>
/// Mask to extract units from raw WM_POINTER data
/// <see cref="/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
private const uint UNIT_MASK = 0x000F;
}
What is the role of the unit here? It's used in conjunction withPOINTER_DEVICE_PROPERTY
For example, the physical dimensions of the touch area Width and Height are calculated using the following algorithm.
short mappedExponent = 0;
if (_hidExponentMap.TryGetValue((byte)( & HidExponentMask), out mappedExponent))
{
float exponent = (float)(10, mappedExponent);
// Guard against divide by zero or negative resolution
if ( - > 0)
{
// Calculated resolution is a scaling factor from logical units into the physical space
// at the given exponentiation.
resolution =
( - ) / (( - ) * exponent);
}
}
/// <summary>
/// Contains the mappings from WM_POINTER exponents to our local supported values.
/// This mapping is taken from WISP code, see Stylus\ - 4,
/// as an array of HidExponents.
/// </summary>
private static Dictionary<byte, short> _hidExponentMap = new Dictionary<byte, short>()
{
{ 5, 5 },
{ 6, 6 },
{ 7, 7 },
{ 8, -8 },
{ 9, -7 },
{ 0xA, -6 },
{ 0xB, -5 },
{ 0xC, -4 },
{ 0xD, -3 },
{ 0xE, -2 },
{ 0xF, -1 },
};
Calculated from the resolution and the value of the specific subsequently received touch point, with the StylusPointPropertyUnit unit, this is the physical size reported by the touch device
that amount or morelogicalMax
cap (a poem)logicalMin
Often referred to in the industry as logic values, the abovephysicalMax
cap (a poem)physicalMin
Often referred to as physical values
After the above, theGetPointerDeviceProperties The list of device properties is converted to the defined properties of the WPF framework.
One detail of the above process is thatGetPointerDeviceProperties The order of the device attribute list is very critical, and the order of the device attribute list corresponds directly to the order of the bare data in the subsequent WM_POINTER message.
As you can see, when the Pointer message is turned on, the initialization of the touch module to get the touch information is done completely through the relevant methods provided by Win32's WM_POINTER module. Here we need to distinguish it from getting touch device information from COM without WM_POINTER message, and thedotnet Read WPF source code notes Insert touch device initialization to get device information The methods offered are not the same
After completing the above initialization logic, let's take a look at the processing of the WM_POINTER message received by the message loop.
When a WM_POINTER message is received, call theGetRawPointerDeviceData Obtain the original touch information, and then parse the original touch information.
Inside WPF, as you know, the underlying message loop is handled in the Defined inside, the input processing section is as follows
namespace
{
/// <summary>
/// The HwndSource class presents content within a Win32 HWND.
/// </summary>
public class HwndSource : PresentationSource, IDisposable, IWin32Window, IKeyboardInputSink
{
private IntPtr InputFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
... // Ignore other codes
// NOTE (alexz): invoke _stylus.FilterMessage before _mouse.FilterMessage
// to give _stylus a chance to eat mouse message generated by stylus
if (!_isDisposed && _stylus != null && !handled)
{
result = _stylus.(hwnd, message, wParam, lParam, ref handled);
}
... // Ignore other codes
}
private SecurityCriticalDataClass<IStylusInputProvider> _stylus;
}
}
The above code for the_stylus
It is the type of HwndPointerInputProvider that decides whether or not to use Pointer message processing based on different configuration parameters, the code is as follows
namespace
{
/// <summary>
/// The HwndSource class presents content within a Win32 HWND.
/// </summary>
public class HwndSource : PresentationSource, IDisposable, IWin32Window, IKeyboardInputSink
{
private void Initialize(HwndSourceParameters parameters)
{
... // Ignore other codes
if ()
{
// Choose between Wisp and Pointer stacks
if ()
{
_stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndPointerInputProvider(this));
}
else
{
_stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndStylusInputProvider(this));
}
}
... // Ignore other codes
}
}
}
In this article, we initialize the HwndPointerInputProvider type, which will enter the FilterMessage method of HwndPointerInputProvider to process the input data.
namespace
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the message loop for the HwndSource, filtering WM_POINTER messages where needed
/// </summary>
/// <param name="hwnd">The hwnd the message is for</param>
/// <param name="msg">The message</param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <param name="handled">If this has been successfully processed</param>
/// <returns></returns>
IntPtr (IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
handled = false;
// Do not process any messages if the stack was disabled via reflection hack
if ()
{
switch (msg)
{
case WindowMessage.WM_ENABLE:
{
IsWindowEnabled = MS..IntPtrToInt32(wParam) == 1;
}
break;
case WindowMessage.WM_POINTERENTER:
{
// Enter can be processed as an InRange.
// The MSDN documentation is not correct for InRange (according to feisu)
// As such, using enter is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), , );
}
break;
case WindowMessage.WM_POINTERUPDATE:
{
handled = ProcessMessage(GetPointerId(wParam), , );
}
break;
case WindowMessage.WM_POINTERDOWN:
{
handled = ProcessMessage(GetPointerId(wParam), , );
}
break;
case WindowMessage.WM_POINTERUP:
{
handled = ProcessMessage(GetPointerId(wParam), , );
}
break;
case WindowMessage.WM_POINTERLEAVE:
{
// Leave can be processed as an OutOfRange.
// The MSDN documentation is not correct for OutOfRange (according to feisu)
// As such, using leave is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), , );
}
break;
}
}
return ;
}
... // Ignore other codes
}
}
For Pointer press and lift messages, the ProcessMessage method is accessed.
Go to the previously calledGetPointerId(wParam)
The GetPointerId method of the code is implemented as follows
/// <summary>
/// Extracts the pointer id
/// </summary>
/// <param name="wParam">The parameter containing the id</param>
/// <returns>The pointer id</returns>
private uint GetPointerId(IntPtr wParam)
{
return (uint)MS.(wParam);
}
internal partial class NativeMethods
{
public static int SignedLOWORD(IntPtr intPtr)
{
return SignedLOWORD(IntPtrToInt32(intPtr));
}
public static int IntPtrToInt32(IntPtr intPtr)
{
return unchecked((int)intPtr.ToInt64());
}
public static int SignedLOWORD(int n)
{
int i = (int)(short)(n & 0xFFFF);
return i;
}
}
Of course, the above code is as simple to write as the following code
var pointerId = (uint) (ToInt32(wparam) & 0xFFFF);
In the design of WM_POINTER, pointer messages will be sent continuously through the message loop, the pointer messages sent do not directly contain specific data information, but only send PointerId as wparam. The only thing we get from the message loop is the value of PointerId, which is converted as shown in the above code.
Why is it designed this way? Considering that most touchscreens nowadays are not low precision, at least higher than many very cheap mice, this may cause the application to be completely unable to withstand the dislike of every touch data coming through the message loop. In the design of WM_POINTER, just the PointerId is sent through the message loop, the specific message body data needs to use theGetPointerInfo method to get it. What are the advantages of this design? This design is used to solve the problem of messages being piled up when the application is stuck. Let's say there are three touch messages coming in, the first one sends a Win32 message to the application, but the application waits until the system has collected all three touch point messages before calling theGetPointerInfo method. At this point, the system touch module can be very open to know the application is in a state of lag, that is, when the second and third touch message arrives, to determine that the first message has not been consumed by the application, and then no longer send Win32 messages to the application. When the application calls theGetPointerInfo method, it returns the third point directly to the application, skipping the first and second touch points in between. Also, using the concept of history points, the first and second and third points are given to the application, if the application is interested at this time
By utilizing the mechanism described above, it is possible to achieve that when a touch device generates touch messages too quickly, instead of keeping the application's message loop overly busy, the application can be given the opportunity to get information about multiple touch points in the past period of time at one time. This improves overall system performance and reduces the need for the application to be busy processing past touch messages.
Let's take a virtual example to better understand the idea of this mechanism. Let's say we're working on an application that has a feature where there's a rectangular element that responds to touch dragging, and you can drag the rectangular element with touch. The app is written in such a way that each drag is done by setting the new coordinate point to the current touch point, but this process takes 15 milliseconds because of some interesting and secretive (I haven't actually coded it yet) algorithms added in the middle. When the application runs on a touch device, the touch device generates touch point information every 10 milliseconds to report to the system during a touch drag. Assuming that the current system's touch module is actually sending Win32 messages to the application every time it receives a touch point from the device, that would make the application's consumption slower than the production of the messages, which means that you can clearly see that dragging a rectangular element has a large sense of latency. This means that you can clearly see a lot of latency when dragging rectangular elements, such as dragging them only to realize that the rectangular elements are still moving slowly behind them, and the overall experience is rather bad. What about the current playbook? The application receives the PointerId information from Win32, and then passes it through theGetPointerInfo method to get the touch point information, the touch point we get is the last touch point, which is perfect for our application, and it is a direct response to set the rectangle element coordinates to the corresponding coordinates of the last touch point. In this way, we can see the rectangle element jumping around, and since the process of dragging the rectangle element is 15 milliseconds, which is less than 16 milliseconds, it means that most of the time, what we see is the smooth movement of the rectangle element, which means that jumping around is a continuous process in the eyes of human beings.
I hope that the above examples can let you understand the "good" intentions of Microsoft!
It should be noted that the PointerId is not the same as the Id of the TouchDevice, etc., and will be described in more detail below.
On the WPF side, as shown in the above code, after receiving the touchpoint information, it will enter the ProcessMessage method, but I feel that there is a little pot in this process is that the timestamp is taken from the value of the current system timestamp, instead of taking the timestamp content inside the Pointer message.
Moving on to the definition and implementation of the ProcessMessage method
namespace
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the latest WM_POINTER message and forwards it to the WPF input stack.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
/// <param name="action">The stylus action being done</param>
/// <param name="timestamp">The time (in ticks) the message arrived</param>
/// <returns>True if successfully processed (handled), false otherwise</returns>
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
... // Ignore other codes
}
}
... // Ignore other codes
}
Inside ProcessMessage the PointerData object will be created, this PointerData type is a helper class and inside the constructor it will be called.GetPointerInfo method to get pointer point information
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
bool handled = false;
// Acquire all pointer data needed
PointerData data = new PointerData(pointerId);
... // Ignore other codes
}
The following is the abridged code for a simple definition of the PointerData constructor
namespace
{
/// <summary>
/// Provides a wrapping class that aggregates Pointer data from a pointer event/message
/// </summary>
internal class PointerData
{
/// <summary>
/// Queries all needed data from a particular pointer message and stores
/// it locally.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
internal PointerData(uint pointerId)
{
if (IsValid = GetPointerInfo(pointerId, ref _info))
{
_history = new POINTER_INFO[_info.historyCount];
// Fill the pointer history
// If we fail just return a blank history
if (!GetPointerInfoHistory(pointerId, ref _info.historyCount, _history))
{
_history = <POINTER_INFO>();
}
... // Ignore other codes
}
}
/// <summary>
/// Standard pointer information
/// </summary>
private POINTER_INFO _info;
/// <summary>
/// The full history available for the current pointer (used for coalesced input)
/// </summary>
private POINTER_INFO[] _history;
/// <summary>
/// If true, we have correctly queried pointer data, false otherwise.
/// </summary>
internal bool IsValid { get; private set; } = false;
}
As you can see by the above code, it starts with a call to theGetPointerInfo method to get pointer information. In WPF's basic events, history points are also supported. The intention is similar to the design intention of Pointer, which is to solve the problem of consuming data speed on the business side. So the bottom of WPF also immediately call theGetPointerInfoHistory Getting historical point information
For Pointer messages, there are different branches of data provision for touch and stylus, namelyGetPointerTouchInfo methodology andGetPointerPenInfo methodologies
Inside the PointerData constructor, it is also determined by thePOINTER_INFO
(used form a nominal expression)pointerType
The fields determine the different methods to call, the code is as follows
if (IsValid = GetPointerInfo(pointerId, ref _info))
{
switch (_info.pointerType)
{
case POINTER_INPUT_TYPE.PT_TOUCH:
{
// If we have a touch device, pull the touch specific information down
IsValid &= GetPointerTouchInfo(pointerId, ref _touchInfo);
}
break;
case POINTER_INPUT_TYPE.PT_PEN:
{
// Otherwise we have a pen device, so pull down pen specific information
IsValid &= GetPointerPenInfo(pointerId, ref _penInfo);
}
break;
default:
{
// Only process touch or pen messages, do not process mouse or touchpad
IsValid = false;
}
break;
}
}
For WPF's HwndPointerInputProvider module, only PT_TOUCH and PT_PEN messages are processed, i.e. touch and stylus messages. For Mouse mouse and Touchpad touchpad there is no Pointer handling, it still takes the original Win32 messages. Why is it designed this way? Because there is no Pointer routing event in WPF, which separates Touch and Stylus and Mouse events. We don't need to handle them all in the Pointer module, we still handle them in the original message loop, which reduces the workload of the Pointer module, and also reduces the workload of distributing from the Pointer to the Touch, Stylus and Mouse events. The original module seems to be running pretty solidly, so we'll just leave it all together.
After completing the PointerData constructor, continue to the HwndPointerInputProvider's ProcessMessage function, which determines if the message is a PT_TOUCH or PT_PEN message.
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
bool handled = false;
// Acquire all pointer data needed
PointerData data = new PointerData(pointerId);
// Only process touch or pen messages, do not process mouse or touchpad
if (
&& ( == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_TOUCH
|| == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_PEN))
{
... // Ignore other codes
}
return handled;
}
For touch and stylus handling, the first step is to perform a touch device association. One manifestation of touch device association in the upper level of business is to associate the current pointer message with the Id of the TouchDevice or the Id of the StylusDevice.
The association is made through theGetPointerCursorId method gets the value of the CursorId first, and then matches it to the input device of the corresponding input Pointer.POINTER_INFO
(used form a nominal expression)sourceDevice
field can be associated with the device created during initialization, the implementation code is as follows
if (
&& ( == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_TOUCH
|| == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_PEN))
{
uint cursorId = 0;
if ((pointerId, ref cursorId))
{
IntPtr deviceId = ;
// If we cannot acquire the latest tablet and stylus then wait for the
// next message.
if (!UpdateCurrentTabletAndStylus(deviceId, cursorId))
{
return false;
}
... // Ignore other codes
}
... // Ignore other codes
}
The input device of the input Pointer in the WPF initialization task.POINTER_INFO
(used form a nominal expression)sourceDevice
take bedeviceId
concept of the Id value of the TabletDevice. While thecursorId
is the Id value of the corresponding StylusDevice, the core of the update code is very simple, such as the following code
/// <summary>
/// Attempts to update the current stylus and tablet devices for the latest WM_POINTER message.
/// Will attempt retries if the tablet collection is invalid or does not contain the proper ids.
/// </summary>
/// <param name="deviceId">The id of the TabletDevice</param>
/// <param name="cursorId">The id of the StylusDevice</param>
/// <returns>True if successfully updated, false otherwise.</returns>
private bool UpdateCurrentTabletAndStylus(IntPtr deviceId, uint cursorId)
{
_currentTabletDevice = tablets?.GetByDeviceId(deviceId);
_currentStylusDevice = _currentTabletDevice?.GetStylusByCursorId(cursorId);
... // Ignore other codes
if (_currentTabletDevice == null || _currentStylusDevice == null)
{
return false;
}
return true;
}
The code for the corresponding GetByDeviceId method is as follows
namespace
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
/// <summary>
/// Holds a mapping of TabletDevices from their WM_POINTER device id
/// </summary>
private Dictionary<IntPtr, PointerTabletDevice> _tabletDeviceMap = new Dictionary<IntPtr, PointerTabletDevice>();
... // Ignore other codes
/// <summary>
/// Retrieve the TabletDevice associated with the device id
/// </summary>
/// <param name="deviceId">The device id</param>
/// <returns>The TabletDevice associated with the device id</returns>
internal PointerTabletDevice GetByDeviceId(IntPtr deviceId)
{
PointerTabletDevice tablet = null;
_tabletDeviceMap.TryGetValue(deviceId, out tablet);
return tablet;
}
}
}
The corresponding code for GetStylusByCursorId is as follows
namespace
{
/// <summary>
/// A WM_POINTER based implementation of the TabletDeviceBase class.
/// </summary>
internal class PointerTabletDevice : TabletDeviceBase
{
/// <summary>
/// A mapping from StylusDevice id to the actual StylusDevice for quick lookup.
/// </summary>
private Dictionary<uint, PointerStylusDevice> _stylusDeviceMap = new Dictionary<uint, PointerStylusDevice>();
/// <summary>
/// Retrieves the StylusDevice associated with the cursor id.
/// </summary>
/// <param name="cursorId">The id of the StylusDevice to retrieve</param>
/// <returns>The StylusDevice associated with the id</returns>
internal PointerStylusDevice GetStylusByCursorId(uint cursorId)
{
PointerStylusDevice stylus = null;
_stylusDeviceMap.TryGetValue(cursorId, out stylus);
return stylus;
}
}
}
A side effect of calling UpdateCurrentTabletAndStylus is that it synchronizes the update of the_currentTabletDevice
cap (a poem)_currentStylusDevice
field, so that subsequent logic can use these two fields directly instead of passing parameters.
After completing the association logic, we enter the GenerateRawStylusData method, which is the core method for WPF to get Pointer-specific messages, and has the following method signature
namespace
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Creates raw stylus data from the raw WM_POINTER properties
/// </summary>
/// <param name="pointerData">The current pointer info</param>
/// <param name="tabletDevice">The current TabletDevice</param>
/// <returns>An array of raw pointer data</returns>
private int[] GenerateRawStylusData(PointerData pointerData, PointerTabletDevice tabletDevice)
{
... // Ignore other codes
}
... // Ignore other codes
}
}
This GenerateRawStylusData call is written as follows
namespace
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the latest WM_POINTER message and forwards it to the WPF input stack.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
/// <param name="action">The stylus action being done</param>
/// <param name="timestamp">The time (in ticks) the message arrived</param>
/// <returns>True if successfully processed (handled), false otherwise</returns>
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
PointerData data = new PointerData(pointerId);
... // Ignore other codes
uint cursorId = 0;
if ((pointerId, ref cursorId))
{
... // Ignore other codes
GenerateRawStylusData(data, _currentTabletDevice);
... // Ignore other codes
}
}
... // Ignore other codes
}
}
Inside the GenerateRawStylusData method, the length of the list of device attributes of the supported Pointer is first fetched via PointerTabletDevice and used to match the information from the input point. Recall that this part of the fetch logic is described above in the introduction to theGetPointerDeviceProperties function, and also shows that the order of the list of device attributes obtained by this function is very critical, the order of the list of device attributes and the order of the bare data obtained in the subsequent WM_POINTER message is directly corresponding to the
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Creates raw stylus data from the raw WM_POINTER properties
/// </summary>
/// <param name="pointerData">The current pointer info</param>
/// <param name="tabletDevice">The current TabletDevice</param>
/// <returns>An array of raw pointer data</returns>
private int[] GenerateRawStylusData(PointerData pointerData, PointerTabletDevice tabletDevice)
{
// Since we are copying raw pointer data, we want to use every property supported by this pointer.
// We may never access some of the unknown (unsupported by WPF) properties, but they should be there
// for consumption by the developer.
int pointerPropertyCount = ;
// The data is as wide as the pointer properties and is per history point
int[] rawPointerData = new int[pointerPropertyCount * ];
... // Ignore other codes
}
... // Ignore other codes
}
By matching the length of each Pointer's attribute with the total number of history points, you can get therawPointerData
The length of the array. This part of the code is believed to be well understood by everyone
Then comes the core part, callingGetRawPointerDeviceData Obtain the original touch information, and then parse the original touch information.
int pointerPropertyCount = ;
// The data is as wide as the pointer properties and is per history point
int[] rawPointerData = new int[pointerPropertyCount * ];
// Get the raw data formatted to our supported properties
if ((
,
,
(uint)pointerPropertyCount,
,
rawPointerData))
{
... // Ignore other codes
}
Inside Pointer's design, the history pointhistoryCount
is containing the current point and the current point is the last point. That's why it's only necessary to pass in the number of history points here, in other words the history points contain at least one point, which is the current point
Because the Pointer to get the point are relative to the screen coordinates, here you need to offset the first modified for the window coordinate system, the code is as follows
// Get the X and Y offsets to translate device coords to the origin of the hwnd
int originOffsetX, originOffsetY;
GetOriginOffsetsLogical(out originOffsetX, out originOffsetY);
private void GetOriginOffsetsLogical(out int originOffsetX, out int originOffsetY)
{
Point originScreenCoord = _source.(new Point(0, 0));
// Use the inverse of our logical tablet to screen matrix to generate tablet coords
MatrixTransform screenToTablet = new MatrixTransform(_currentTabletDevice.TabletToScreen);
screenToTablet = (MatrixTransform);
Point originTabletCoord = originScreenCoord * ;
originOffsetX = (int)();
originOffsetY = (int)();
}
/// <summary>
/// The HwndSource for WM_POINTER messages
/// </summary>
private SecurityCriticalDataClass<HwndSource> _source;
The logic of GetOriginOffsetsLogical here is to go to the 0,0 point of the window and see where that point will be on the screen to know its offset. As for the added MatrixTransform Matrix TabletToScreen, we'll talk about it later in this article, so we'll skip it for now.
Once you have obtained the coordinate offsets relative to the window, you can superimpose them on each point to convert them to the window coordinate system. But before you can do that, you need to add the obtained coordinates of therawPointerData
Processing. This step is only required in WPF, just to be compatible with the way WISP gets the bare data. The difference is that the data obtained via PointerrawPointerData
The binary data format does not contain information about the support of the buttons, so WPF needs to create a new array of buttons for therawPointerData
Rearranging to ensure that each point's data is added to the button's informational data
This part of the process is just for compatibility purposes, so that the subsequent StylusPointCollection will be a good place to start, so let's just skip ahead.
int numButtons = - ;
int rawDataPointSize = (numButtons > 0) ? pointerPropertyCount - numButtons + 1 : pointerPropertyCount;
// Instead of a single entry for each button we use one entry for all buttons so reflect that in the raw data size
data = new int[rawDataPointSize * ];
for (int i = 0, j = - pointerPropertyCount; i < ; i += rawDataPointSize, j -= pointerPropertyCount)
{
(rawPointerData, j, data, i, rawDataPointSize);
// Apply offsets from the origin to raw pointer data here
data[i + ] -= originOffsetX;
data[i + ] -= originOffsetY;
... // Ignore other codes
}
... // Ignore other codes
return data;
The process of recopying also replaces the coordinates of the point with the window coordinate system, i.e. the abovedata[i + ] -= originOffsetX;
cap (a poem)data[i + ] -= originOffsetY;
Two codes
Once the fetch is complete, the bare data is returned, which is the contents of GenerateRawStylusData.
Get the raw pointer information returned by GenerateRawStylusData in the ProcessMessage method, and give it to the RawStylusInputReport as a parameter.
// Generate a raw input to send to the input manager to start the event chain in PointerLogic
Int32[] rawData = GenerateRawStylusData(data, _currentTabletDevice);
RawStylusInputReport rsir =
new RawStylusInputReport(
,
timestamp,
_source.Value,
action,
() => { return _currentTabletDevice.StylusPointDescription; },
_currentTabletDevice.Id,
_currentStylusDevice.Id,
rawData)
{
StylusDevice = _currentStylusDevice.StylusDevice,
};
Updates the created RawStylusInputReport to the current device as the last pointer information of the device
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
PointerData data = new PointerData(pointerId);
... // Ignore other codes
_currentStylusDevice.Update(this, _source.Value, data, rsir);
... // Ignore other codes
}
private SecurityCriticalDataClass<HwndSource> _source;
It is also added to the ProcessInput of the InputManager, and enters the message scheduling within the framework of WPF.
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
PointerData data = new PointerData(pointerId);
... // Ignore other codes
_currentStylusDevice.Update(this, _source.Value, data, rsir);
// Now send the input report
(irea);
... // Ignore other codes
}
Before getting to the InputManager's ProcessInput scheduling message, take a look at the_currentStylusDevice.Update
The implementation logic for parsing the raw pointer information inside the
exist_currentStylusDevice.Update
The parsing of the raw pointer information is implemented entirely by the StylusPointCollection and the StylusPoint constructor.
namespace
{
/// <summary>
/// A WM_POINTER specific implementation of the StylusDeviceBase.
///
/// Supports direct access to WM_POINTER structures and basing behavior off of the WM_POINTER data.
/// </summary>
internal class PointerStylusDevice : StylusDeviceBase
{
/// <summary>
/// Updates the internal StylusDevice state based on the WM_POINTER input and the formed raw data.
/// </summary>
/// <param name="provider">The hwnd associated WM_POINTER provider</param>
/// <param name="inputSource">The PresentationSource where this message originated</param>
/// <param name="pointerData">The aggregated pointer data retrieved from the WM_POINTER stack</param>
/// <param name="rsir">The raw stylus input generated from the pointer data</param>
internal void Update(HwndPointerInputProvider provider, PresentationSource inputSource,
PointerData pointerData, RawStylusInputReport rsir)
{
... // Ignore other codes
// First get the initial stylus points. Raw data from pointer input comes in screen coordinates, keep that here since that is what we expect.
_currentStylusPoints = new StylusPointCollection(, (), GetTabletToElementTransform(null), );
... // Ignore other codes
}
}
}
Here.()
is to return the above mentionedGenerateRawStylusData
method gives a copy of the bare data with the following code
internal class RawStylusInputReport : InputReport
{
/// <summary>
/// Read-only access to the raw data that was reported.
/// </summary>
internal int[] GetRawPacketData()
{
if (_data == null)
return null;
return (int[])_data.Clone();
}
/// <summary>
/// The raw data for this input report
/// </summary>
int[] _data;
... // Ignore other codes
}
Here GetTabletToElementTransform contains a core transformation with the following method code
internal class PointerStylusDevice : StylusDeviceBase
{
/// <summary>
/// Returns the transform for converting from tablet to element
/// relative coordinates.
/// </summary>
internal GeneralTransform GetTabletToElementTransform(IInputElement relativeTo)
{
GeneralTransformGroup group = new GeneralTransformGroup();
Matrix toDevice = _inputSource.;
();
(new MatrixTransform( * toDevice));
((relativeTo));
return group;
}
... // Ignore other codes
}
The key element of this method is the calculation of the TabletToScreen property of the PointerTabletDevice. The calculation of this matrix requires the use of theGetPointerDeviceRects function to get thedisplayRect
Size, andGetPointerDeviceProperties The X and Y attribute descriptions are obtained with the following definition code for the attributes
internal Matrix TabletToScreen
{
get
{
return new Matrix(_tabletInfo. / _tabletInfo., 0,
0, _tabletInfo. / _tabletInfo.,
0, 0);
}
}
You can see that this is a Matrix object for scaling, which is exactly what theGetPointerDeviceRects The screen size obtained and theGetPointerDeviceProperties Ratio of TabletSize to the descriptive information of the X and Y attributes obtained.
Recap._tabletInfo
You can see that TabletSize is entirely determined by the size of the descriptor, as follows
// The following code is in the Papers
// private bool TryInitializeSupportedStylusPointProperties()
SupportedPointerProperties = new UnsafeNativeMethods.POINTER_DEVICE_PROPERTY[propCount];
success = (Device, ref propCount, SupportedPointerProperties);
... // Ignore other codes
// private bool TryInitializeDeviceRects()
var deviceRect = new ();
var displayRect = new ();
success = (_deviceInfo.device, ref deviceRect, ref displayRect);
if (success)
{
// We use the max X and Y properties here as this is more readily useful for raw data
// which is where all conversions come from.
SizeInfo = new TabletDeviceSizeInfo
(
new Size(SupportedPointerProperties[].logicalMax,
SupportedPointerProperties[].logicalMax),
new Size( - , - )
);
}
internal struct TabletDeviceSizeInfo
{
public Size TabletSize;
public Size ScreenSize;
internal TabletDeviceSizeInfo(Size tabletSize, Size screenSize)
{
TabletSize = tabletSize;
ScreenSize = screenSize;
}
}
In this way, you can use the TabletToScreen property to convert the coordinates of a Tablet coordinate system-based bare pointer message to screen coordinates, and then invert it with TransformToDevice to convert to the WPF coordinate system.
In the above code, since the GetTabletToElementTransform is passed into therelativeTo
parameter is a null value, will result in the(relativeTo)
returns a unitary matrix, which means that inside the GetTabletToElementTransform method, the((relativeTo));
is redundant, maybe I'll optimize it for future WPF versions.
Looking back at the StylusPointCollection's constructor parameters, the only useful parameters are the first three, which are Pass in descriptor information, and
()
Returns bare pointer data, andGetTabletToElementTransform(null)
method returns the matrix converted to the WPF coordinate system.
_currentStylusPoints = new StylusPointCollection(, (), GetTabletToElementTransform(null), );
Then the last parameter of the StylusPointCollection, the one passed in by the above code, is the What is the use? In fact, in the design of StylusPointCollection, the third parameter and the fourth parameter are optional, and the third parameter has a higher priority than the fourth parameter. That is, at the bottom of StylusPointCollection, it will judge whether the third parameter has a value or not, if not, it will use the fourth parameter.
Inside the StylusPointCollection constructor, the bare Pointer data will be processed, now the int array of the bare Pointer data obtained by GetRawPacketData is arranged as follows
| X-coordinate | Y-coordinate | Pressure Sense (optional) | The list of properties inside StylusPointDescription corresponds one to the other.
| X-coordinate | Y-coordinate | Pressure Sense (optional) | The list of properties inside StylusPointDescription corresponds one to the other | X-coordinate | Y-coordinate | Pressure Sense (optional)
| X-coordinate | Y-coordinate | Pressure Sense (optional) | The list of properties inside StylusPointDescription corresponds to each other |
Stored is one or more points of information, each point of information is the same binary length, sub-packaging is very simple
Go to the StylusPointCollection constructor and look at the code signature definition.
namespace
{
public class StylusPointCollection : Collection<StylusPoint>
{
internal StylusPointCollection(StylusPointDescription stylusPointDescription, int[] rawPacketData, GeneralTransform tabletToView, Matrix tabletToViewMatrix)
{
... // Ignore other codes
}
}
}
Inside the constructor, call the GetInputArrayLengthPerPoint method of StylusPointDescription to get the binary length of each point.
public class StylusPointCollection : Collection<StylusPoint>
{
internal StylusPointCollection(StylusPointDescription stylusPointDescription, int[] rawPacketData, GeneralTransform tabletToView, Matrix tabletToViewMatrix)
{
... // Ignore other codes
int lengthPerPoint = ();
... // Ignore other codes
}
}
Having obtained the binary length of a point, it is natural to figure out the incomingrawPacketData
The parameter contains information about how many points
internal StylusPointCollection(StylusPointDescription stylusPointDescription, int[] rawPacketData, GeneralTransform tabletToView, Matrix tabletToViewMatrix)
{
... // Ignore other codes
int lengthPerPoint = ();
int logicalPointCount = / lengthPerPoint;
(0 == % lengthPerPoint, "Invalid assumption about packet length, there shouldn't be any remainder");
... // Ignore other codes
}
The above code for the is to ensure that the incoming
rawPacketData
It is possible to belengthPerPoint
i.e., each point is divisible by its binary length
After completing the preparations, the next step is to place therawPacketData
Solve the point, as shown in the following code
int lengthPerPoint = ();
int logicalPointCount = / lengthPerPoint;
for (int count = 0, i = 0; count < logicalPointCount; count++, i += lengthPerPoint)
{
//first, determine the x, y values by xf-ing them
Point p = new Point(rawPacketData[i], rawPacketData[i + 1]);
... // Ignore other codes
int startIndex = 2;
... // Ignore other codes
int[] data = null;
int dataLength = lengthPerPoint - startIndex;
if (dataLength > 0)
{
//copy the rest of the data
var rawArrayStartIndex = i + startIndex;
data = (rawArrayStartIndex, dataLength).ToArray();
}
StylusPoint newPoint = new StylusPoint(, , , _stylusPointDescription, data, false, false);
... // Ignore other codes
((List<StylusPoint>)).Add(newPoint);
}
The parts of the code that the above code omits contain details such as the coordinate conversion for Point, which is done using thePoint p = new Point(rawPacketData[i], rawPacketData[i + 1]);
The coordinates of the point you get are Tablet coordinates and need to be converted to WPF coordinates using the incoming parameters, as shown in the following code
internal StylusPointCollection(StylusPointDescription stylusPointDescription, int[] rawPacketData, GeneralTransform tabletToView, Matrix tabletToViewMatrix)
{
... // Ignore other codes
Point p = new Point(rawPacketData[i], rawPacketData[i + 1]);
if (tabletToView != null)
{
(p, out p);
}
else
{
p = (p);
}
... // Ignore other codes
}
As you can see from the code above, the StylusPointCollection constructor uses either the third or fourth parameter for the transformation, with the third parameter preferred if it exists.
The logic for other processing is the additional handling of pressure sensing, which is an explicit parameter of StylusPoint and requires additional judgment to handle
int startIndex = 2; // X cap (a poem) Y Occupies two elements
bool containsTruePressure = ;
if (containsTruePressure)
{
// If there's pressure.,Pressure sensing also needs to take up an extra element
//don't copy pressure in the int[] for extra data
startIndex++;
}
StylusPoint newPoint = new StylusPoint(, , , _stylusPointDescription, data, false, false);
if (containsTruePressure)
{
// Pressure sensing must be the third element,Update pressure sensing if you have pressure sensing
//use the algorithm to set pressure in StylusPoint
int pressure = rawPacketData[i + 2];
(, pressure);
}
This will unpack it.| X-coordinate | Y-coordinate | Pressure Sense (optional) | StylusPointDescription The list of properties inside the StylusPointDescription corresponds |
The first three elements of the Ribbon, of which pressure sensing is optional. The subsequentThe list of properties inside StylusPointDescription corresponds to each other.
Part of this requires recreating the data arrays to be passed into each StylusPoint, as follows
int[] data = null;
int dataLength = lengthPerPoint - startIndex;
if (dataLength > 0)
{
//copy the rest of the data
var rawArrayStartIndex = i + startIndex;
data = (rawArrayStartIndex, dataLength).ToArray();
}
Subsequent properties of StylusPoint can be obtained through the description information, the description information to get the value is to take the above code passed to thedata
The element of the binary array with the corresponding subscript, e.g. the width or height information of a touch point
Once the conversion to StylusPointCollection is complete, you can use the method dispatches the bare input information to the WPF Input Manager.
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
... // Ignore other codes
InputReportEventArgs irea = new InputReportEventArgs(_currentStylusDevice.StylusDevice, rsir)
{
RoutedEvent = ,
};
// Now send the input report
(irea);
... // Ignore other codes
}
Inside ProcessInput, the standard routing event mechanism will be used to trigger the Touch or Stylus event through the routing mechanism, and the next logic will look at the call stack, which is similar to the logic of other input events.
> !.MainWindow_TouchDown(object sender, e)
!( handler, object target)
!(object source, args, bool reRaised)
!( sender, args)
!( args)
!()
!()
!()
!()
!( stylusDevice, stagingItem)
!(<, []> postProcessInput, processInputEventArgs)
!()
!(uint pointerId, action, int timestamp)
!(nint hwnd, msg, nint wParam, nint lParam, ref bool handled)
!(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
Since I'm running a Release version of WPF, there are some functions that are inlined, such as the functions from the until (a time)
Less in the middle.
function, a fully functionless inline stack should look like this
!()
!()
!(uint pointerId, action, int timestamp)
For example, the following code is the code for the ProcessInput function
public sealed class InputManager : DispatcherObject
{
public bool ProcessInput(InputEventArgs input)
{
... // Ignore other codes
PushMarker();
PushInput(input, null);
RequestContinueProcessingStagingArea();
bool handled = ProcessStagingArea();
return handled;
}
}
Entering the ProcessStagingArea method will execute the specific scheduling logic, using the above touchdown stack as an example, it will enter the PointerLogic's PostProcessInput method, which will call the PromoteMainToOther method, then the PromoteMainToTouch method, and finally the PromoteMainDownToTouch method. from the PostProcessInput method to PromoteMainToOther to PromoteMainToTouch and finally to PromoteMainDownToTouch. Only the intermediate methods are inlined, directly from the stack from RaiseProcessInputEventHandlers to PromoteMainDownToTouch method, the stack is as follows
!(...)
!(...)
The core press code is in PromoteMainDownToTouch, which looks like this
private void PromoteMainDownToTouch(PointerStylusDevice stylusDevice, StagingAreaInputItem stagingItem)
{
PointerTouchDevice touchDevice = ;
... // Ignore other codes
();
();
}
From the above, we know that inside the ProcessMessage of the HwndPointerInputProvider, it calls the_currentStylusDevice.Update
method, the input data is stored inside the PointerStylusDevice.
The logic of the follow-up is similar to that of theWPF Analog Touch Devices The mentioned usage is pretty much the same, except that the data supply source is provided from PointerStylusDevice. If you're interested in getting to the subsequent logic of the InputManager, see theWPF Simulates Scheduling Touch Events with InputManager Run the provided method yourself
For more touches seeWPF Touch Related