Introduction
Hello and welcome to my first article on .NET programming and specifically managed C++. Learning managed C++ (MC++ in short) was a fun and a new experience, and in order for me to retain this newly learned skill, I applied most of the things I learned in fully documented examples outlining how and what to use.
Unlike my other articles, this article will teach you MC++ in a direct and experiential manner. I don't claim you will learn everything, but you will get to know the essentials and then you can continue on your own. This article can be considered as a quick reference for a lot of things you're going to need through your work.
In order to derive the most benefits out of this article, it is advisable that you know C/C++, some knowledge about the .NET Framework and preferably a bit of knowledge in C#.
Twelve examples will be presented, a short introduction text will precede every example, and each example will be heavily documented. You may build each example through the following command line:
"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\vsvars32.bat"
cl your_file.cpp /clr
Please note that some of the things mentioned in the examples work with 7.0 and 7.1 and might not work w/ MC++ 8.0.
The covered topics are:
- Between managed and unmanaged code
- Managed Classes
- Managed Strings
- Enumerations and boxing
- Pinning Pointers
- Properties
- Delegates
- Value types, Structs and Unions
- Managed Arrays
- Platform Invoke (PInvoke)
- Multithreading
- Using Windows Forms
- Equivalence between C/C++ and .NET Framework
Between managed and unmanaged code
This is the first example. We need to put: "#using <mscorlib.dll>
" in order to start using the .NET Framework. Use the "cl filename.cpp /clr" to compile into managed code. You can have unmanaged code inside the managed program by using the unmanaged/managed pragma as shown in the example.
In managed mode, you cannot use __asm
and many other features. That's why it is sometimes important to be able to use managed and unmanaged.
#using <mscorlib.dll> // to use Console::WriteLine
#include <stdio.h> // to printf()
using namespace System;
#pragma unmanaged
void print(char *msg)
{
printf("%s\n", msg);
}
#pragma managed
int main()
{
Console::WriteLine(S"Hello world from managed method");
print("hello world from unmanaged method");
return 0;
}
Managed Classes
To indicate that a class is managed you have to prefix the declaration with the "__gc
" modifier. This modifier can be used with classes, structs and variables that are explicitly marked to be managed by the garbage collector.
Notice you don't need to delete the allocated class instance, for the garbage collector takes care of that!
You may omit the "__gc
" modifier, thus a native C++ class will be created. Or you may use the "__nogc
" to have the same effects.
You may create static constructors, however they are not allowed to have parameters. The static constructor is called just before the first access is made to a member.
#using <mscorlib.dll>
using namespace System;
__gc class CHelloWorld
{
System::String *_msg;
public:
CHelloWorld(System::String *Msg)
{
_msg = Msg;
}
void Print()
{
Console::WriteLine(_msg);
}
};
__gc class CHelloWorldStatic
{
static System::String *_static_msg =
S"Static message called because static constructor invoked!";
static Int32 _instance_cnt = 0;
public:
CHelloWorldStatic()
{
_instance_cnt++;
Console::WriteLine(System::String::Concat("So far ",
_instance_cnt.ToString(), " instance(s)"));
}
static CHelloWorldStatic()
{
Console::WriteLine(_static_msg);
}
};
int main()
{
CHelloWorld *c1 = __gc new CHelloWorld(S"Hello world from the class");
c1->Print();
CHelloWorldStatic *c2 = __gc new CHelloWorldStatic();
return 0;
}
Managed Strings
In .NET, strings are managed objects. They give you a wide range of functionality. The System::String
object is immutable. That is, if you change the string, you'll get a completely new string that is the modified version of that string. If you want to be able to change / modify the string buffer, you should use the System::Text::StringBuilder
object instead.
The String
object holds each character in Unicode through the "Char
" type. To access the characters individually you can do something like:
String *str = S"hello";
Char c = str->Chars[3];
When you declare the string prefixed with "S
", this means that the string is a managed string.
You cannot compare two strings as "if (s1 == s2)
" because that will compare the strings' references and return true
if they are the same reference. Instead you should call String::Equals()
or String::Compare()
.
You may not pass managed string to a C++ standard library or CRT function. You can use Marshal::StringtoHGlobalUni
method in the System::Runtime::InteropServices
. You should always free the returned string using Marshal::FreeHGlobal()
.
VC++ provides a helper function, PtrToStringChars()
, defined in "vcclr.h" that allows you to access the internal wchar_t*
of a managed string.
You may use Marshal::PtrToStringAnsi
or Uni
to convert from ANSI or Unicode to a managed string.
Examples below will illustrate everything.
#using <mscorlib.dll>
#include <vcclr.h> // PtrToStringChars()
#include <stdio.h>
using namespace System;
using namespace System::Runtime::InteropServices;
void test_ptrtostringchars()
{
String *s = S"Hello world\n";
const Char __pin *p = PtrToStringChars(s);
wprintf(p);
}
void test_stringcomparison()
{
String *s1 = S"Hi";
String *s2 = S"Hello";
String *temp1 = s1->Substring(0, 1);
String *temp2 = s2->Substring(0, 1);
if (temp1 == temp2)
{
Console::WriteLine("wow cool...the strings were equal");
}
if (temp1->Equals(temp2))
Console::WriteLine("wow cool...the strings were Equal()");
}
void test_stringbuilder()
{
Text::StringBuilder *sb = new Text::StringBuilder;
sb->Append("This is first line\n");
for (int i=2;i<=10;i++)
{
sb->AppendFormat("This is line #{0}\n", i.ToString());
}
Console::WriteLine("The total string built is: {0}", sb->ToString());
}
void test_hglobal()
{
String* s = S"This is a String!";
char *ansi = (char *) Marshal::StringToHGlobalAnsi(s).ToPointer();
wchar_t *unicode = (wchar_t *) Marshal::StringToHGlobalUni(s).ToPointer();
printf("printf_ansi(): %s\n", ansi);
wprintf(L"printf_unicode(): %s\n", unicode);
Marshal::FreeHGlobal(ansi);
Marshal::FreeHGlobal(unicode);
}
String *ansi_to_managed(char *str)
{
return Marshal::PtrToStringAnsi(str);
}
String *unicode_to_managed(wchar_t *str)
{
return Marshal::PtrToStringUni(str);
}
int main()
{
String *s1 = ansi_to_managed("Hello world! ! ! ! ! ");
String *s2 = unicode_to_managed(L"Hello world! ! ! ! ! ");
test_ptrtostringchars();
test_stringcomparison();
test_stringbuilder();
test_hglobal();
return 0;
}
Enumerations and boxing
Enums are value-types and have similar characteristics, they are derived from System::Enum
-> System::ValueType
thus can be converted to other types. The enumerated values are integral types.
You may specify the [Flags]
attribute.
When you apply the ToString()
on an enum
, two things can occur:
- when not using
[Flags]
: the ToString()
will return the numeric value of the enum
s (if a combination of enum
s was used).
- when not using
[Flags]
: the ToString()
will return the name of the enum
if one enum
is used.
- when using
[Flags]
: the ToString()
will return the names of the enum
s used, comma separated, if a combination of enum
s was used.
- when using
[Flags]
: the ToString()
will return the name of the enum
(when one enum
is used).
.NET allows you to convert a value type to a __gc
object through a process named boxing. You can only box value-types. When you box a value, the runtime creates a new object on the heap containing the same value that is being boxed.
When a change is made to the boxed value, the modification only affects the boxed type and not the value type that was initially boxed.
As said before, when you box a value-type, you are converting from a value-type to a __gc
type. Now here's how you can convert back and from these types (this process is called unboxing):
MyInt p2(2); MyInt __gc *p3 = __box(p2); MyInt p4 = *p3;
#using <mscorlib.dll>
using namespace System;
using namespace System::Collections;
__value enum Color : unsigned int
{
RED = 0xFF0000,
GREEN = 0x00FF00,
BLUE = 0x0000FF
};
[Flags]
__value enum Features: unsigned int
{
feature_1 = 1,
feature_2 = 2,
feature_3 = 4,
feature_all = feature_1 | feature_2 | feature_3
};
__value struct MyInt
{
Int32 val;
MyInt(Int32 v)
{
val = v;
}
};
int main()
{
Console::WriteLine(S"RED as string: {0}\n(RED | GREEN) as string: {1:x}",
__box(RED)->ToString(), __box((Color)RED | GREEN)->ToString());
Console::WriteLine();
Console::WriteLine(S"(feature_1 | feature_2) as string: {0}",
__box((Features)(feature_1 | feature_2))->ToString());
Console::WriteLine();
Array *list = Enum::GetNames(__typeof(Color));
for (Int32 i=0;i<list->Length;i++)
{
Console::WriteLine(S"Item {0} is called {1}", i.ToString(),
list->GetValue(i)->ToString());
}
Console::WriteLine();
Color c;
Object *o = Enum::Parse(__typeof(Color), S"RED");
c = *static_cast<__box Color *>(o);
Console::WriteLine(S"{0}", __box(c)->ToString());
Console::WriteLine();
MyInt vt1(14656); MyInt *b = __box(vt1);
Console::WriteLine(S"b->val={0}", b->val.ToString());
b->val++;
Console::WriteLine(S"After incrementing b->val,
vt1.val={0} and b->val={1}",
vt1.val.ToString(), b->val.ToString());
return 0;
}
Pinning Pointers
Managed pointers are managed and tracked by the garbage collector. When you want to pass a gc pointer to a nongc method, you need a way to tell the garbage collector not to move or discard that object. In all cases you cannot pass a gc pointer to a non gc method directly.
When you pin a managed object you may pass the pinned pointer to the unmanaged method.
Pinned objects will increment the object reference count and will inform the garbage collector so that it doesn't move the object in memory.
#using <mscorlib.dll>
#include <stdio.h>
using namespace System;
#pragma unmanaged
void print_msg(char *s)
{
printf("printf() -> %s\n", s);
}
#pragma managed
int main()
{
Byte arr[] = new Byte[20];
arr[0] = 'h'; arr[1] = 'e'; arr[2] = 'l';
arr[3] = 'l'; arr[4] = 'o'; arr[5] = 0;
unsigned char __pin *str = &arr[0];
print_msg((char *)str);
return 0;
}
Properties
Usually, variables defined in a class or struct are called fields. These fields can be modified without really being validated or checked. Properties are a means to allow you validate/monitor a field's read or write through get
/set
methods.
To create properties you have to use the __property
keyword before the prototype, then you prefix the function name w/ either "set_" or "get_".
You may also use indexed get
properties, that is instead of having a property like: "Name
", you can have a property like Name["index"]
. Or numeric index as Name[1234]
. You may even have a two dimensional index as: Name[2, 3] ...
#using <mscorlib.dll>
using namespace System;
__gc class Student
{
private:
String *_name;
public:
__property String *get_Name()
{
if (_name == 0)
{
_name = new String(S"Noname");
}
return _name;
}
__property void set_Name(String *n)
{
if (n == 0)
throw new ArgumentException(S"You must pass a name!");
_name = n;
}
__property String *get_Hey(String *index)
{
return index;
}
__property int get_Mul(int x, int y)
{
return x * y;
}
};
int main()
{
Student *s = __gc new Student();
Console::WriteLine(S"Student name={0}", s->Name);
s->Name = S"Hey!";
Console::WriteLine(S"Student name={0}", s->Name);
Console::WriteLine(S"Student name={0}", s->Hey["Hello"]);
Console::WriteLine(S"x*y={0}", (s->Mul[2][3]).ToString());
return 0;
}
Delegates
Delegates are similar to callback functions in C/C++. A delegate can only be bound to one or more methods within a __gc
class.
When you initialize an instance of a delegate you have to pass two parameters:
- A pointer to a
__gc
class or NULL
(if you are binding to a static method within that __gc
class).
- A pointer to the method that you are binding to.
Once the delegate instance is created, you will have an Invoke
method that has the same signature/prototype as the delegate definition. So you might equally call the delegate as: delegate_name(param_list)
or delegate_name->Invoke(param_list)
.
Delegates inherit from System::MulticastDelegate
therefore you may list, combine or remove delegates from the chain.
You may also refer to MSDN and the "__delegate
" keyword reference for more information.
#using <mscorlib.dll>
using namespace System;
using namespace System::Collections;
public __delegate int CallMethod(String *);
__gc public class Caller
{
public:
int CallMe(String *s)
{
Console::WriteLine(s);
return s->Length;
}
static int CallMeToo(String *s)
{
Console::WriteLine(S"static: {0}", s);
return s->Length;
}
};
void delegate_member_test()
{
Caller *c = new Caller();
CallMethod *m = new CallMethod(c, &Caller::CallMe);
int i;
i = m(S"Hello");
i = m->Invoke(S"Hello");
}
void delegate_static_test()
{
Caller *c = new Caller();
CallMethod *m = new CallMethod(c, &Caller::CallMe);
int i;
i = m(S"Hello");
i = m->Invoke(S"Hello");
}
public __delegate float BinaryOperationDelegate(float x, float y);
public __gc class BinaryArithmeticClass
{
private:
float _a, _b;
BinaryOperationDelegate *_op;
public:
BinaryArithmeticClass(float a, float b) : _a(a), _b(b), _op(0) { }
BinaryArithmeticClass() { }
void SetOperation(BinaryOperationDelegate *handler)
{
_op = handler;
}
float Calculate()
{
return _op(a, b);
}
__property void set_a(float a) { _a = a; }
__property float get_a() { return _a; }
__property void set_b(float b) { _b = b; }
__property float get_b() { return _b; }
};
__gc public class SimpleAddOperation
{
public:
static float Execute(float a, float b)
{
return a + b;
}
};
__gc public class TwoDimToOneDimConverter
{
private:
float _width;
public:
TwoDimToOneDimConverter(float width) : _width(width) { }
float Execute(float x, float y)
{
return (y*_width) + x;
}
};
void delegate_advanced_test()
{
BinaryArithmeticClass *arith = __gc new BinaryArithmeticClass();
BinaryOperationDelegate *add = new BinaryOperationDelegate(0,
&SimpleAddOperation::Execute);
arith->b = 5;
arith->a = 10;
arith->SetOperation(add);
Console::WriteLine("BinaryArithmeticClass using add delegate = {0}",
arith->Calculate().ToString());
TwoDimToOneDimConverter *conv1 = __gc new TwoDimToOneDimConverter(10);
BinaryOperationDelegate *convdelegate = new BinaryOperationDelegate(conv1,
&TwoDimToOneDimConverter::Execute);
arith->a = 2;
arith->b = 1;
arith->SetOperation(convdelegate);
Console::WriteLine("BinaryArithmeticClass using 2d-to-1d delegate = {0}",
arith->Calculate().ToString());
}
public __delegate void MessageDisplayDelegate();
__gc public class MessageDisplay
{
private:
String *_msg;
public:
MessageDisplay(String *msg) : _msg(msg) { }
void Display()
{
Console::WriteLine(_msg);
}
};
void delegate_juggling()
{
MessageDisplay *m1 = __gc new MessageDisplay(S"Msg1");
MessageDisplay *m2 = __gc new MessageDisplay(S"Msg2");
MessageDisplayDelegate *del = new MessageDisplayDelegate(m1,
&MessageDisplay::Display);
del += new MessageDisplayDelegate(m2, &MessageDisplay::Display);
del();
Delegate *d[] = del->GetInvocationList();
IEnumerator *e = d->GetEnumerator();
int idx = 1;
while (e->MoveNext())
{
MessageDisplayDelegate *delegateI =
dynamic_cast<MessageDisplayDelegate *>(e->Current);
Console::Write("Delegate #{0} ->", idx.ToString());
delegateI->Invoke();
idx++;
}
}
int main()
{
delegate_member_test();
delegate_static_test();
delegate_advanced_test();
delegate_juggling();
return 0;
}
Value types, Structs and Unions
Value types are typically small short-lived objects and they are usually created in the stack. To create value types, you mark the declaration with __value
.
Usually, value-types are used as records of data, as if using structs in C. You cannot use initialization list in the constructor of a value-type class. Instead you have to initialize the variables from within the body. They are stored sequentially in memory but the amount of bytes they occupy is determined by the ".pack
" metadata for the method (the default packing is eight).
You may change this behaviour by using their pseudo custom attribute [StructLayout]
.
This attribute can type the following three enumeration values:
Auto
: the runtime determines the order and amount of memory taken by members.
Sequential
: The amount will be at least as large as the size of the member, however they are sequentially ordered.
Explicit
: you specify the exact layout of members: their byte location and the size of each member.
C++ managed does not have unions, thus you may use the layout Explicit
to simulate the unions.
#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
__value class Point
{
public:
int _x;
int _y;
};
[StructLayout(LayoutKind::Explicit)]
__value struct LargeInteger
{
[FieldOffset(0)] int lowPart;
[FieldOffset(4)] int highPart;
[FieldOffset(0)] __int64 quadPart;
};
int main()
{
Point pt1 = {1, 2};
LargeInteger li;
li.quadPart = 0x22;
Console::WriteLine("{0:X}", li.quadPart.ToString());
return 0;
}
Managed Arrays
- Array Rank = Indicates the dimension of the array. A two-dimensional array has a rank of 2.
- LowerBound(dim) = Returns the lower bound of the given dimension.
- UpperBound(dim) = Returns the upper bound of the given dimension.
By default the lower bound is zero and upper bound is the array's length-1.
#using <mscorlib.dll>
using namespace System;
void display_string_array1(String *ar[])
{
for (int i=0;i<ar->Length;i++)
Console::Write(S"{0} ", ar[i]);
Console::WriteLine();
}
void display_string_array2(String *ar[,])
{
for (int i=ar->GetLowerBound(0);i<=ar->GetUpperBound(0);i++)
{
for (int j=ar->GetLowerBound(1);j<=ar->GetUpperBound(1);j++)
Console::WriteLine(S"arr[{0},{1}] = {2}",
i.ToString(), j.ToString(), ar[i,j]);
}
}
void test1()
{
String *names[] = __gc new String*[3];
names[0] = S"Hello";
names[1] = S"World";
names[2] = S"of Wonders!";
display_string_array1(names);
String *arr2[,] = new String *[3, 2];
arr2[0,0] = S"First1";
arr2[0,1] = S"Last1";
arr2[1,0] = S"First2";
arr2[1,1] = S"Last2";
arr2[2,0] = S"First3";
arr2[2,1] = S"Last3";
display_string_array2(arr2);
}
void test2()
{
Array *a = Array::CreateInstance(__typeof(String), 3);
String *names[] = dynamic_cast<String *[]>(a);
names[0] = S"Hey,";
names[1] = S"are you";
names[2] = S"fine?";
display_string_array1(names);
int dim __gc[] = {2, 4};
Array *b = Array::CreateInstance(__typeof(String), dim);
String *vals __gc[,] = dynamic_cast<String *[,]>(b);
Console::WriteLine(S"Rank is: {0}", b->Rank.ToString());
for (int i=vals->GetLowerBound(0);i<=vals->
GetUpperBound(0);i++)
{
for (int j=vals->GetLowerBound(1);
j<=vals->GetUpperBound(1);j++)
{
vals[i,j] = String::Format("{0},{1}", __box(i), __box(j));
Console::WriteLine(vals[i,j]);
}
}
}
int main()
{
test1();
test2();
return 0;
}
Platform Invoke (PInvoke)
PInvoke is a short name for Platform Invoke, it enables managed code to call C-style functions in native DLLs.
An important and unique feature of Managed Extensions for C++ is that you can use unmanaged APIs directly. If you do not require customized data marshaling, you do not need to use PInvoke.
Managed strings can be passed as in
parameters via interop as:
[DllImport("kernel32", CharSet=CharSet::Auto)]
unsigned GetFileAttributes(String *file)
The thunk will convert this string to an unmanaged buffer according to the CharSet
field.
If you include standard Windows headers and use the .lib files, you can easily call external functions without using PInvoke.
Make sure you include the Windows header before the #using <mscorlib.dll>
so to avoid names conflict, or simply delay the "using namespace System
" till after you include the Windows header.
If you happen to include Windows headers, you are still bound to face name clashes. For instance, calling Forms::MessageBox::Show()
will conflict with the #define MessageBox
entry from the Windows header.
One solution is to do:
#ifdef MessageBox
#undef MessageBox
#endif
The example below should illustrate many things:
#include <windows.h>
#using <mscorlib.dll>
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
using namespace System;
using namespace System::Runtime::InteropServices;
namespace Win32
{
[DllImport("kernel32", CharSet=CharSet::Auto,
EntryPoint="GetFileAttributes")]
unsigned GetFileAttributesCall(String *Path);
[DllImport("user32")]
unsigned MessageBeep(unsigned uType);
[DllImport("kernel32")]
unsigned GetLogicalDrives();
[DllImport("msvcrt", EntryPoint="rand")]
unsigned my_rand();
[DllImport("msvcrt")]
unsigned srand(unsigned seed);
[DllImport("kernel32", CharSet=CharSet::Auto,
EntryPoint="GetWindowsDirectory")]
unsigned GetWindowsDirectoryCall(Text::StringBuilder *, unsigned);
String *getwindir()
{
unsigned len = GetWindowsDirectoryCall(0, 0);
Text::StringBuilder *sb = new Text::StringBuilder(len);
GetWindowsDirectoryCall(sb, sb->Capacity);
return sb->ToString();
}
};
void test_pinvoke()
{
String *filename = S"C:\autoexec.bat";
unsigned attr = Win32::GetFileAttributesCall(filename);
Console::WriteLine(S"\"{0}\"'s attributes: {1:X}\n",
filename, attr.ToString());
Console::WriteLine(S"Windows directory is located at: {0}\n",
Win32::getwindir());
Win32::srand((unsigned)Environment::TickCount);
unsigned drives = Win32::GetLogicalDrives();
for (unsigned i=0;i<26;i++)
{
if (((1 << i) & drives) == 0)
continue;
Console::WriteLine(S"Drive {0}:\ present",
((Char)('A'+i)).ToString());
}
Console::WriteLine("\nA call to rand() returned: {0}",
Win32::my_rand().ToString());
}
void test_direct_calls()
{
::MessageBoxA(::GetDesktopWindow(), "Info", "Hello world", MB_OK);
DWORD len = ::GetCurrentDirectory(0, 0);
TCHAR *str = new TCHAR[len];
String *s = 0;
if (::GetCurrentDirectory(len, str) != 0)
s = new String(str);
delete [] str;
Console::WriteLine(S"Current directory: {0}\n",
s != 0 ? s : S"error");
}
void test_dynamic_load_calls()
{
typedef int (__stdcall *msgboxa_proc)(int, char *, char *, int);
HMODULE h = ::LoadLibrary("user32");
if (h == 0)
return;
msgboxa_proc m = (msgboxa_proc) ::GetProcAddress(h, "MessageBoxA");
if (m != NULL)
m(0, "Hello world", "info", MB_OK);
::FreeLibrary(h);
}
int main()
{
test_pinvoke();
test_direct_calls();
test_dynamic_load_calls();
return 0;
}
Multithreading
You may create threads in MC++ through the System::Threading
namespace and its Thread
class. The Thread
class is sealed
, thus you cannot derive from it. Beware of the usual sychronization problems that occur with multithreading when accessing the same variable. You may use, for instance, the Interlocked::Increment(&your_integer)
to increment that variable in a safe manner.
As you know, a static member is shared between all instances of a class. You may us the [ThreadStatic]
attribute so that there will be a static member for each thread. If the member is accessed from another thread it will have a different value.
#using <mscorlib.dll>
using namespace System;
using namespace System::Threading;
__gc class MyProgress
{
private:
int _start, _end, _speed;
public:
MyProgress(int start, int end, int speed)
{
_start = start;
_end = end;
_speed = speed;
}
void Progress()
{
while (_start <= _end)
{
Console::Write("{0}/{1} \r",
(_start++).ToString(), _end.ToString());
Thread::Sleep(_speed);
}
Console::WriteLine("finished ");
}
};
void display_thread_info(Thread *t)
{
Console::WriteLine("Thread name: {0}", t->Name);
Console::WriteLine("Thread priority: {0}",
__box(t->Priority));
}
int main()
{
display_thread_info(Thread::CurrentThread);
MyProgress *pb = __gc new MyProgress(0, 20, 20);
Thread *t = new Thread(new ThreadStart(pb,
&MyProgress::Progress));
t->Name = pb->ToString();
display_thread_info(t);
t->Start();
t->Join();
Console::WriteLine("--press enter to terminate application-");
Console::ReadLine();
return 0;
}
Using Windows Forms
This example has no introductory text about Windows Forms. You are supposed to know the .NET framework in order to understand how most of the used components work.
This example will show you how it is possible to create Windows Forms and controls dynamically from MC++.
#using <mscorlib.dll>
#using <system.dll>
#using <system.drawing.dll>
#using <system.windows.forms.dll>
using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Runtime::InteropServices;
using namespace System::Reflection;
using namespace System::IO;
__gc class TestForm : public Form
{
protected:
void OnPaint(PaintEventArgs *args)
{
Graphics *g = args->Graphics;
DrawBackground(g);
}
void DrawBackground(Graphics *g)
{
g->DrawLine(Pens::Black, 0, 0, ClientSize.Width-1,
ClientSize.Height-1);
g->DrawLine(Pens::Black, 0, ClientSize.Height-1,
ClientSize.Width-1, 0);
}
void WndProc(Message *m)
{
Form::WndProc(m);
}
private:
void SetBackGround()
{
String *strName =
String::Concat(RuntimeEnvironment::GetRuntimeDirectory(),
S"\system.windows.forms.dll");
Assembly *assem = Assembly::LoadFrom(strName);
Stream *stm =
assem->GetManifestResourceStream(S"System.Windows"
S".Forms.Panel.bmp");
Bitmap *bmp = new Bitmap(stm);
BackgroundImage = bmp;
}
Button *_buttons __gc[];
void InitButtons()
{
int cnt = 10;
_buttons = __gc new Button*[cnt];
for (int i=0;i<cnt;i++)
{
Button *b = new Button;
_buttons[i] = b;
b->Width = 40;
b->Height = 40;
b->Text = String::Format("B#{0}", (i+1).ToString());
b->Visible = true;
b->Left = (i*40) + 30;
b->Top = 15;
b->Tag = String::Format(S"I am button #{0}", (i+1).ToString());
Controls->Add(b);
b->Click += new EventHandler(this, BtnClick);
}
}
void BtnClick(Object *sender, EventArgs *args)
{
Button *btn = dynamic_cast<Button *>(sender);
MessageBox::Show(btn->Tag->ToString());
}
public:
TestForm()
{
Text = S"Hello world!";
Width = 470;
Height = 100;
SetBackGround();
InitButtons();
}
};
int main()
{
Application::Run(new TestForm);
return 0;
}
Equivalence between C/C++ and .NET Framework
Below is a table that will show you C/C++/Win32 API syntax/functions and their equivalent MC++ and .NET Framework syntax.
C/C++/Win32 API Syntax | MC++ / .NET Framework |
printf("The string is: %s", str); | System::Console::WriteLine(S"The string is: {0}", str); // where 'str' is a String * (or else you may went to box it or ToString()) |
sprintf() | refer to String::Format( ) |
strcat / strncat | refer to String::Concat , StringBuilder::Append /AppendFormat |
strchr | String::IndexOf |
strlen | String::Length (property) |
strupr / lwr | String::ToUpper /ToLower |
isalpha , isdigit , isspace | Char::IsLetter , Char::IsDigit , Char::IsWhitespace |
atoi , atol , atof , strtol , strtod | refer to the object's Parse() method. Example: Int32::Parse |
itoa , itol , ... | refer to the object's ToString() method. Example: Int32 i; i.ToString(); |
gets() | Console::ReadLine() |
findclose , findfirst , findnext | Directory::GetDirectories and Directory::GetFiles |
getenv | Environment::GetEnvironmentVariables |
_execl , _spawnl | Process:Start |
asctime , ctime , _ftime | DateTime::Now |
_argc , argv[] | Environment::GetCommandLineArgs |
STL containers: list , map , queue , set , vector , stack , ... | Array , HashTable , ArrayList , Stack , ... |
Format specifiers table
Usually you format a string through first passing the format then a list of arguments in the order of appearance/usage.
printf("%d %d %d\n", 1 ,2 ,3);
In .NET formatting strings, you specify the format / order like this:
FormatFunction("Hello {0} and {1} or {1} and {0}"
" in no specific order", "John", "Marie");
And the output would be:
Hello John and Marie or Marie and John in no specific order
As you notice you have the advantage because if you need to display an argument again, you don't have to pass it twice, all you have to do is just reference it by the number {n}
. The general format item syntax is: {index[,alignment][:formatString]}
.
Now you can format the output through format specifiers defined in the table below:
Specifier | Description |
{n:C} | Currency format |
{n:Dm} | Integer with m digits |
{n:Em} | Scientific format; m is the precision |
{n:Xm} | Hex format, m is number of digits |
For more reference on formatting, refer to ".NET Framework Developer's Guide/Standard Numeric Format Strings".
Conclusion
I hope you learned and enjoyed through reading this article. It should be enough to get you started in little time, the rest is up to you.
I do expect a lot of questions and comments, however bear in mind that I have very limited and fresh experience with the .NET Framework and might not be able to answer all your questions. I have included a comprehensive set of references so that you can get help from them, have fun!
Reference