I have moved!

I've moved my blog
CLICK HERE

Saturday, 7 June 2008

C++/CLI/WPF

The executive summary of this post is as follows:

Application().Run(%Window());

That line of code is all you need to make a blank window open in today's Visual C++. And if you're a veteran of the dark days of Petzold and Win32, that has to be viewed as a major improvement. (Even if that % operator does look like gibberish.)

In fact from here on in, I'll assume you are such a veteran but are only vaguely aware of .NET.

Win32 API has a native language - C, and by extension, C++. Other languages lack the equivalent of all the .h and .lib files that are the doorway to Win32. Even C# only provides the syntax for declaring entry points, and the user has to then use that syntax to manually declare them (although community efforts like http://pinvoke.net/ have naturally sprung up to fill this gap).

And some APIs require direct manipulation of native memory just to call them. This is notoriously difficult to get right in C and C++. Try the security or Spooler or Lan Manager APIs for example. Of course C# can't really do anything to make it easier, as every case is slightly different, but at least in C++ there is sample code to guide you, and it can be tricky to translate it into the equivalent unsafe C#.

So Visual C++ still has a place in 2008. And then there's legacy code that needs new features. The question is, how easy is it to start from an old-style C++ app and then seamlessly mix today's CLR libraries into it?

To test this in Visual Studio 2008, create a C++ Win32 Console Application. This serves as our example starting point, all old and crummy, and the IDE has written the crummy old main function for us like this:

#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[])
{
    return 0;
}

Obviously we can include <windows.h> and do anything we like with Win32. But then we look at Petzold and see how many pages of code are needed just to get a blank window to open on the screen, and we think to ourselves: there must be an easier way.

Go to the project's Properties, and under Configuration Properties change the Common Language Runtime support setting to have the value: Common Language Runtime Support (/clr).

Then right-click the project and choose References. Click Add Reference and choose the following magical ingredients:

  • PresentationCore
  • PresentationFramework
  • WindowsBase

Note that just by adding the references, our C++ language namespace already contains all the types we need. There's no need to include any extra headers. But we can still use a using namespace declaration to help us to abbreviate our code, same as in standard C++. So put this between the #include statement and the _tmain function:

using namespace System::Windows;

(It has to be after the stdafx.h header, because of the weird behaviour of Visual C++ precompiled headers, sigh.)

Directly above the _tmain function, paste this magical incantation:

[System::STAThread]

This is an attribute, and it has to be there for WPF to work. Finally, we're ready to enter a new world! The excitement is electric here at the Stroustrup Stadium.

Put this inside the _tmain function.

Application().Run(%Window());

Now hit F5. A blank window opens! When you close it, the program terminates (in fact, control returns from the Run function, so the Window behaves like a modal dialog box).

As a C++ veteran, you'd assume that Application is either a function that returns an object (or a reference to an object) or else its the constructor of a type, being used here to make a temporary instance. Well, I can tell you it's the latter: constructor of a temporary. Same for the Window type. We construct an Application, construct a Window, and then call Application::Run, passing it... something windowy. If we tweaked the code a little, you'd understand it immediately:

Application().Run(&Window());

The % operator is very much the CLR equivalent of &. As always in C++, we distinguish between an object and a pointer-to-object. When declaring a pointer-like variable, we use ^ instead of * (and we call the variable a handle instead of a pointer, and we use gcnew instead of new to construct a free store instance). It's almost as if a whole separate peer language has been dropped into C++, but the new language still has the characteristic flavour of the old.

So both of these have the same outcome as the original snippet:

Window w;
Application().Run(%w);

or...

Window ^w = gcnew Window();
Application().Run(w);

The analogy even extends to C++ references, which are like pointers that can only be assigned to once and then are syntactically used like objects instead of pointers. For example,

Window ^w = gcnew Window();

Application ^a1 = gcnew Application();
a1->Run(w);

Because a1 is a handle, we have to use the arrow (->) operator to call functions on it, instead of the dot, just like we do with pointers. But just as in standard C++, we can avoid that:

Application ^a1 = gcnew Application();
    
Application %a2 = *a1;
a2.Run(w);

Again, note how in the declarations, ^ is like * and % is like &. But in the usage, the syntax is just like Standard C++: asterisk to dereference, and arrow to access members - this extra bit of uniformity was suggested by Stroustrup to allow templates to be written such that they would work on pointers or handles.

The part I really like is the way destructors work, and I wish C# had the same thing (it's currently only part of the way there, with the using statement). But anyway, to finish off the simple example, add this extra namespace declaration:

using namespace System::Windows::Controls;

Then try this code:

TextBlock b;
b.Text = "Hello, world.";

Window w;
w.Content = %b;
Application().Run(%w);

Here's the complete source:

#include "stdafx.h"

using namespace System::Windows;
using namespace System::Windows::Controls;

[System::STAThread]
int _tmain(int argc, _TCHAR* argv[])
{
    TextBlock b;
    b.Text = "Hello, world.";

    Window w;
    w.Content = %b;
    Application().Run(%w);

    return 0;
}

No comments: