Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions | ![]() |
In the following walkthrough we will show how Qt objects can be used in a .NET environment using the ActiveQt framework as an interoperability bridge between the .NET world and Qt.
Qt is a C++ library and is compiled into traditional, native binaries that make full use of the performance provided by the runtime environment, and even though Qt provides mechanisms to simplify memory management it does not provide any garbage collection.
One of the key concept of .NET is the idea of "managed code" - the source code is compiled into an intermediate bytecode, and that bytecode is run through a virtual machine - the Common Language Runtime (CLR). This virtual machine provides among other things garbage collection for allocated objects.
While it is possible to compile Qt application code into bytecode for the CLR, and to use .NET framework classes in Qt code, it is impossible to go the other way round - bytecode can not directly call native code. (1)
We will now demonstrate how to use classes written with Qt in a .NET environment, both by implementing a manual wrapper class, and by utilizing the ActiveQt framework as a generic bridge.
In the second part of the walkthrough we will create a simple user interface in Visual Studio.NET, with the functionality implemented using C#. This interface will use the QPushButton class as well as the QAxWidget2 class.
The example directory of ActiveQt includes the result of this walkthrough using both C# and VB.NET, created with Visual Studio.NET (not 2003). Load examples/dotnet/walkthrough/csharp.csproj, examples/dotnet/walkthrough/vb.vbproj (2) or examples/dotnet/wrapper/wrapper.sln into the IDE and run the solution.
To use native C++ classes from .NET bytecode you have to provide a wrapper for each of your classes. Those wrappers can be implemented manually in the form of C++ classes. However, this is only possible in VC++.NET, thanks to extensions to the C++ language that Microsoft introduced with the 7.x series of their C++ compiler. (3) Microsoft calls this the IJW approach - "It Just Works".
// native Qt/C++ class class Worker : public QObject { Q_OBJECT Q_PROPERTY(QString statusString READ statusString WRITE setStatusString) public: Worker(); QString statusString() const; public slots: void setStatusString(const QString &string); signals: void statusStringChanged(const QString &string); private: QString status; };
The Qt class has nothing unusual for Qt users, and as even the Qt specialities like Q_PROPERTY, slots and signals are implemented with straight C++ they don't cause any trouble when compiling this class with any C++ compiler.
class Worker; // .NET class public __gc class netWorker { public: netWorker(); ~netWorker(); __property String *get_StatusString(); __property void set_StatusString(String *string); __event void statusStringChanged(String *args); private: Worker *workerObject; };
The .NET wrapper class uses keywords that are part of Microsoft's C++ extension to indicate that the class is garbage collected (__gc), and that StatusString should be accessible as a property in languages that support this concept (__property). We also declare an event function statusStringChanged(String*) (__event), the equivalent of the respective signal in the Qt class.
Before we can start implementing the wrapper class we need a way to convert Qt's datatypes (and potentionally your own) into .NET datatypes, e.g. QString objects need to be converted into objects of type String*.
#include <qstring.h> #using <mscorlib.dll> #include <vcclr.h> using namespace System; String *QStringToString(const QString &qstring) { return new String(qstring.ucs2()); }
QString StringToQString(String *string) { wchar_t __pin *chars = PtrToStringChars(string); return QString::fromUcs2(chars); }
The convertor functions can then be used in the wrapper class implementation to call the functions in the native C++ class.
#include "networker.h" #include "worker.h" #include "tools.h" netWorker::netWorker() { workerObject = new Worker(); }
netWorker::~netWorker() { delete workerObject; }
The constructor and destructor simply create and destroy the Qt object wrapped using the C++ operators new and delete. Note that the constructor of the Worker class doesn't take any parameters, ie. it cannot be a child of another QObject. Supporting this would make it necessary to reflect changes in the object hierarchy (ie. when objects are destroyed, or due to calls to QWidget::reparent()) in the .NET-copy of the object hierarchy.
Notifying the .NET wrapper of such changes from within Qt would require some native Qt object that can communicate to the respective .NET object. Bytecode objects however cannot be referenced in native code (the garbage collector can after all delete the object living in bytecode any moment), so there is no simple way to communicate in that direction. (4)
String *netWorker::get_StatusString() { return QStringToString(workerObject->statusString()); }
The netWorker class delegates calls from the .NET code to the native code. The transition between those two worlds implies a small performance hit for each function call, in addition to the type conversion.
void netWorker::set_StatusString(String *string) { workerObject->setStatusString(StringToQString(string)); __raise statusStringChanged(string); }
The property setter calls the native Qt class before firing the event using the __raise keyword. This is necessary as Qt and .Net use incompatible signal and slot implementations, so we cannot just connect the Qt signal to the .Net event. The solution provided here is not working as soon as the Qt class emits signals based on internal status changes, which is of course how signals usually work.
The inability of .NET classes to live in native code makes it again very difficult to provide a generic solution.
That wrapper can now be used in .NET code, e.g. using C++, C#, Visual Basic or any other programming language available for .NET.
using System; namespace WrapperApp { class App { void Run() { netWorker worker = new netWorker(); worker.statusStringChanged += new netWorker.__Delegate_statusStringChanged(onStatusStringChanged); System.Console.Out.WriteLine(worker.StatusString); System.Console.Out.WriteLine("Working cycle begins..."); worker.StatusString = "Working"; worker.StatusString = "Lunch Break"; worker.StatusString = "Working"; worker.StatusString = "Idle"; System.Console.Out.WriteLine("Working cycle ends..."); } private void onStatusStringChanged(string str) { System.Console.Out.WriteLine(str); } [STAThread] static void Main(string[] args) { App app = new App(); app.Run(); } } }
This is clearly a big effort already for a simple class, and we haven't even been able to completely model important Qt concepts like parent/child relations or signals and slots in the .NET wrapper class.
And all this effort is necessary to be able to support your code on a single additional platform - Microsoft .NET - and is useless for any other environment, where the wrapper class will not even compile as it uses Microsoft specific C++ extensions.
Fortunately .NET provides a generic wrapper for COM objects, the Runtime Callable Wrapper (RCW). This RCW is a proxy for the COM object and is generated by the CLR when a .NET Framework client activates a COM object. This provides a generic way to reuse COM objects in a .NET Framework project.
Making a QObject class into a COM object is easily achieved with ActiveQt and demonstrated in the examples. The walkthrough will use the Qt classes implemented in those examples, so the first thing to do is to make sure that those examples have been built correctly, e.g. by opening the demonstration pages in Internet Explorer to verify that the controls are functional.
Start Visual Studio.NET, and create a new C# project for writing a Windows application. This will present you with an empty form in Visual Studio's dialog editor. You should see the toolbox, which presents you with a number of available controls and objects in different categories. If you right-click on the toolbox it allows you to add new tabs. We will add the tab "Qt".
The category only has a pointer tool by default, and we have to add the Qt objects we want to use in our form. Right-click on the empty space, and select "Customize". This opens a dialog that has two tabs, "COM Components" and ".NET Framework Components". We used ActiveQt to wrap QWidgets into COM objects, so we select the "COM Components" page, and look for the classes we want to use, e.g. "QPushButton" and "QAxWidget2".
When we select those widgets and close the dialog the two widgets will now be available from the toolbox as grey squares with their name next to it (5) .
We can now add an instance of QAxWidget2 and a QPushButton to the form. Visual Studio will automatically generate the RCW for the object servers. The QAxWidget2 instance takes most of the upper part of the form, with the QPushButton in the lower right corner.
In the property editor of Visual Studio we can modify the properties of our controls - QPushButton exposes the QWidget API and has many properties, while QAxWidget2 has only the Visual Studio standard properties in addition to its own property "lineWidth" in the "Miscellaneous" category. The objects are named "axQPushButton1" and "axQAxWidget21", and since especially the last name is a bit confusing we rename the objects to "resetButton" and "circleWidget".
We can also change the Qt properties, e.g. set the "text" property of the resetButton to "Reset", and the "lineWidth" property of the circleWidget to 5. We can also put those objects into the layout system that Visual Studio's dialog editor provides, e.g. by setting the anchors of the circleWidget to "Left, Top, Right, Bottom", and the anchors of the resetButton to "Bottom, Right".
Now we can compile and start the project, which will open a user interface with our two Qt widgets. If we can resize the dialog, the widgets will resize appropriately.
We will now implement event handlers for the widgets. Select the circleWidget and select the "Events" page in the property editor. The widget exposes events because the QAxWidget2 class has the "StockEvents" attribute set in its class definition. We implement the event handler circleClicked for the ClickEvent to increase the line width by one for every click:
private void circleClicked(object sender, System.EventArgs e) { this.circleWidget.lineWidth++; }
In general we can implement a default event handler by double clicking on the widget in the form, but the default events for our widgets are right now not defined.
We will also implement an event handler for the clicked signal emitted by QPushButton. Add the event handler resetLineWidth to the clicked event, and implement the generated function:
private void resetLineWidth(object sender, System.EventArgs e) { this.circleWidget.lineWidth = 1; this.resetButton.setFocus(); }
We reset the property to 1, and also call the setFocus() slot to simulate the user style on Windows, where a button grabs focus when you click it (so that you can click it again with the spacebar).
If we now compile and run the project we can click on the circle widget to increase its line width, and press the reset button to set the line width back to 1.
Using ActiveQt as a universal interoperability bridge between the .NET world and the native world of Qt is very easy, and makes it often unnecessary to implement a lot of handwritten wrapper classes. Instead, the QAxFactory implementation in the otherwise completely cross-platform Qt project provides the glue that .NET needs to to generate the RCW.
If this is not sufficient we can implement our own wrapper classes thanks to the C++ extensions provided by Microsoft.
All the limitations when using ActiveQt are implied when using this technique to interoperate with .NET, e.g. the datatypes we can use in the APIs can only be those supported by ActiveQt and COM. However, since this includes subclasses of QObject and QWidget we can wrap any of our datatypes into a QObject subclass to make its API available to .NET. This has the positive side effect that the same API is automatically available in QSA, the cross platform scripting solution for Qt applications, and to COM clients in general.
When using the "IJW" method the only limitation is that it's error prone as a lot of code is copied, and that it is very time consuming to do, especially if you have custom datatypes to support. Additionally it is difficult to model concepts like signals and slots in the .NET wrapper, and the parent/child hierarchies in Qt are difficult to combine with the garbage collected world of CLR bytecode.
Every call from CLR bytecode to native code implies a small performance hit, and necessary type conversions introduce an additional delay with every layer that exists between the two frameworks. Consequently every approach to mix .NET and native code should try to minimize the communication necessary between the different worlds.
As ActiveQt introduces three layers at once - the RCW, COM and finally ActiveQt itself - the performance penalty when using the generic Qt/ActiveQt/COM/RCW/.NET bridge as opposed to a hand-crafter IJW-wrapper class can be significant. (6) The execution speed however is still sufficient for connecting to and modifying interactive elements in a user interface, and as soon as the benefit of using Qt and C++ to implement and compile performance critical algorithms into native code kicks in ActiveQt becomes a valid choice for making even non-visual parts of your application accessible to .NET.
' VB is case insensitive, but our C++ controls are not. ' Me.resetButton.enabled = TrueThis line is regenerated without comment whenever you change the dialog, in which case you have to comment it out again to be able to run the project. This is a bug in the original version of Visual Studio.NET, and is fixed in the 2003 edition. Back...
See also The QAxServer Examples.
Copyright © 2003 Trolltech | Trademarks | Qt 3.3.0b1
|