PROGRAMMING SIMCONNECT CLIENTS
USING MANAGED CODE
There is a managed wrapper for SimConnect that enables .NET
language programmers to write SimConnect clients. This section describes how to set up a managed client project, and the key differences to look out for when programming to the wrapper.
The following sections use C# as the primary example language, although we also provide examples using VB.
Setup For .NET
Ensure that .NET Framework Version 4.7 is installed and then:
- Create a new project using Visual Studio 2017 or higher, typically this would be a C# Windows Application, or a VB.NET Windows application.
- Add a new reference to
Microsoft.FlightSimulator.SimConnect
in your project. This file can be found in the$(MSFS SDK)\SimConnect SDK\lib\managed
folder. - Add the following two lines to the projects
using
statements (in C#, note the VB.NET equivalent to using is imports):using Microsoft.FlightSimulator.SimConnect; using System.Runtime.InteropServices;
- Write the application using the functions in the SimConnect library, taking into account the following notes.
Notes On .NET Client Programming
The managed SimConnect assembly is installed in the GAC (global assembly cache) during the installation of the SimConnect SDK, so it should not be manually placed anywhere for your application to run. However, you do need to reference a copy of this dll in Visual Studio so that it can resolve symbols at compile time. As Visual Studio does not support referencing an assembly stored in the GAC, a copy of the SimConnect.dll outside the simconnect SDK msi is shipped solely for this purpose.
The native function calls SimConnect_Open and SimConnect_Close have been replaced by the SimConnect constructor, and Dispose method respectively. This means that there is no handle required for the function calls, so for the most part the managed calls use the same parameters as the native calls, except without the SimConnect handle. The code to open and close a SimConnect client using C# is:
SimConnect simconnect = null;
const int WM_USER_SIMCONNECT = 0x0402;
try
{
simconnect = new SimConnect("Managed Data Request", this.Handle, WM_USER_SIMCONNECT, null, 0);
}
catch (COMException ex)
{
}
if (simconnect != null)
{
simconnect.Dispose();
simconnect = null;
}
And using VB.NET it would be:
Rem Open
Try
fsx_simconnect = New SimConnect(" VB Managed Data Request", Me.Handle, WM_USER_SIMCONNECT, Nothing, 0)
Catch ex As Exception
Rem Failed to connect
End Try
Rem Close
If fsx_simconnect IsNot Nothing Then
fsx_simconnect.Dispose()
fsx_simconnect = Nothing
End If
It is worth noting the following:
-
What would have been a failed HRESULT returned in the native API translates to a COMException.
-
The use of raw memory pointers is not permitted in the managed API. You must define your structs and attribute them properly so that the system marshaller can handle them.
-
When the client receives either a SIMCONNECT_RECV_SIMOBJECT_DATA_BYTYPE or a SIMCONNECT_RECV_SIMOBJECT_DATA structure, the dwData members will contain your struct as System.Object. This should be cast to the proper type.
-
Partial data return (SIMCONNECT_DATA_REQUEST_FLAG_TAGGED) is unsupported. For a description of this flag see the SimConnect_AddToDataDefinition function.
-
The top level ReceiveDispatch switch case statement is not necessary. The client should register a handler for the appropriate OnRecvXXX event, and call ReceiveMessage when it is notified that messages are waiting in the queue. For Windows applications use a win32 handle (a Control.Handle) to SimConnect to receive notifications when a message arrives. The code for this - using C# - is as follows:
protected override void DefWndProc(ref Message m)
{
if (m.Msg == WM_USER_SIMCONNECT)
{
if (simconnect != null)
{
simconnect.ReceiveMessage();
}
}
else
{
base.DefWndProc(ref m);
}
}
And using VB.NET it would be:
Protected Overrides Sub DefWndProc(ByRef m As Message)
If m.Msg = WM_USER_SIMCONNECT Then
If fsx_simconnect IsNot Nothing Then
fsx_simconnect.ReceiveMessage()
End If
Else
MyBase.DefWndProc(m)
End If
End Sub
Note that:
-
Constants in the native simconnect.h are declared as static members of the SimConnect class. For example: SIMCONNECT_OBJECT_ID_USER becomes SimConnect.SIMCONNECT_OBJECT_ID_USER
-
Enums in managed code are scoped. The names of the enum members have been changed, for example, SIMCONNECT_RECV_ID_QUIT in native code is SIMCONNECT_RECV_ID.QUIT in C#, or Microsoft.FlightSimulator.SimConnect.SIMCONNECT_RECV_ID.QUIT in VB.NET. Note that in VB.NET the full reference is necessary to locate a structure or enumeration.
-
Structs are mostly unchanged, with the exception that a char array is represented as a System.String. However, they do need to be registered with the managed wrapper with a call to RegisterDataDefineStruct. If a string is too long during the data marshaling, it will get truncated safely. Some specific information must be provided with each structure, as shown in the C# sample below:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
struct Struct1
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public String title;
public double latitude;
public double longitude;
public double altitude;
};
simconnect.AddToDataDefinition((uint)DEFINITIONS.Struct1, "Title", null, SIMCONNECT_DATATYPE.STRING256, 0, SimConnect.SIMCONNECT_UNUSED);
simconnect.AddToDataDefinition((uint)DEFINITIONS.Struct1, "Plane Latitude", "degrees", SIMCONNECT_DATATYPE.FLOAT64, 0, SimConnect.SIMCONNECT_UNUSED);
simconnect.AddToDataDefinition((uint)DEFINITIONS.Struct1, "Plane Longitude", "degrees", SIMCONNECT_DATATYPE.FLOAT64, 0, SimConnect.SIMCONNECT_UNUSED);
simconnect.AddToDataDefinition((uint)DEFINITIONS.Struct1, "Plane Altitude", "feet", SIMCONNECT_DATATYPE.FLOAT64, 0, SimConnect.SIMCONNECT_UNUSED);
simconnect.RegisterDataDefineStruct<Struct1>((uint)DEFINITIONS.Struct1);
The same sample using VB.NET would be:
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi, Pack:=1)> _
Structure Struct1
Rem This is how you declare a fixed size string
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=256)> _
Public title As String
Public latitude As Double
Public longitude As Double
Public altitude As Double
End Structure
Rem define a data structure, note the last parameter, datumID must be different for each item
fsx_simconnect.AddToDataDefinition(StructDefinitions.Struct1, "Title", "", Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATATYPE.STRING256, 0, 0)
fsx_simconnect.AddToDataDefinition(StructDefinitions.Struct1, "Plane Latitude", "degrees", Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATATYPE.FLOAT64, 0, 1)
fsx_simconnect.AddToDataDefinition(StructDefinitions.Struct1, "Plane Longitude", "degrees", Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATATYPE.FLOAT64, 0, 2)
fsx_simconnect.AddToDataDefinition(StructDefinitions.Struct1, "Plane Altitude", "feet", Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATATYPE.FLOAT64, 0, 3)
Rem IMPORTANT: register it with the simconnect managed wrapper marshaller
Rem if you skip this step, you will only receive an int in the .dwData field.
fsx_simconnect.RegisterDataDefineStruct(Of Struct1)(StructDefinitions.Struct1)
Things to note:
-
Variable length strings are not supported in the managed layer.
-
Optional parameters are not supported in the managed layer
To send data in an array, copy the array to a polymorphic object array. For example, to apply a waypoint list to an AI aircraft, go through the following steps using C#:
simconnect.AddToDataDefinition(DEFINITIONS.1,"AI WAYPOINT LIST", "number", SIMCONNECT_DATATYPE.WAYPOINT, 0.0f, SimConnect.SIMCONNECT_UNUSED);
SIMCONNECT_DATA_WAYPOINT[] waypoints = new SIMCONNECT_DATA_WAYPOINT[2];
waypoints[0].Flags = (uint)SIMCONNECT_WAYPOINT_FLAGS.SPEED_REQUESTED;
waypoints[0].ktsSpeed = 100;
waypoints[0].Latitude = 10;
waypoints[0].Longitude = 20;
waypoints[0].Altitude = 1000;
waypoints[1].Flags = (uint)SIMCONNECT_WAYPOINT_FLAGS.SPEED_REQUESTED;
waypoints[1].ktsSpeed = 150;
waypoints[1].Latitude = 11;
waypoints[1].Longitude = 21;
waypoints[1].Altitude = 2000;
Object[] objv = new Object[ waypoints.Length ];
waypoints.CopyTo(objv, 0);
simconnect.SetDataOnSimObject(DEFINITIONS.1, AIAircraftID, SIMCONNECT_DATA_SET_FLAG.DEFAULT, objv);
The same waypoint code using VB.NET would be like this:
REM Note that the client code should already have declared a
REM data definition, StructDefinitions.DEFINITIONS.1, and
REM called fsx_SimConnect_RequestDataOnSimObjectType to return an AI aircraft ID.
REM Step 1: Add a the waypoint list to the data definition
fsx_simconnect.AddToDataDefinition(StructDefinitions.DEFINITION1, "AI WAYPOINT LIST", "number", Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATATYPE.WAYPOINT, 0.0F, Microsoft.FlightSimulator.SimConnect.SimConnect.SIMCONNECT_UNUSED)
REM Step 2: Declare an array of the appropriate size
Dim waypoints(2) As Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATA_WAYPOINT
REM Step 3: Populate the array with all the required data
waypoints(0).Flags = Microsoft.FlightSimulator.SimConnect.SIMCONNECT_WAYPOINT_FLAGS.SPEED_REQUESTED
waypoints(0).ktsSpeed = 100
waypoints(0).Latitude = 10
waypoints(0).Longitude = 20
waypoints(0).Altitude = 1000
waypoints(1).Flags = Microsoft.FlightSimulator.SimConnect.SIMCONNECT_WAYPOINT_FLAGS.SPEED_REQUESTED
waypoints(1).ktsSpeed = 100
waypoints(0).Latitude = 11
waypoints(0).Longitude = 21
waypoints(0).Altitude = 1000
REM Step 4: The managed wrapper marshaling code expects a polymorphic array
Dim objv(waypoints.Length) As Object
waypoints.CopyTo(objv, 0)
REM Step 5: Now make the call to apply the waypoint structure to the AI aircraft
fsx_simconnect.SetDataOnSimObject(StructDefinitions.DEFINITION1, AIAircraftID, Microsoft.FlightSimulator.SimConnect.SIMCONNECT_DATA_SET_FLAG.DEFAULT, objv)