Contents
The program presented here allows you to draw different graphics and to embed graphics into an executable. When you run that executable, it will produce exactly the same drawing on the desktop. What's more, you will see the graphics being drawn. So, you can draw or write something using your mouse in different colors and different widths, build an executable file and send it to someone. When they run it, it will reproduce everything you have drawn.
- How to draw directly on the desktop
- How to deserialize an object into a different type than the one it was serialized into
- How to compile C# code during runtime
It would be nice if you were familiar with basic serialization concepts.
When you run the program, you can start drawing by pressing the mouse button and moving it while holding it down. You can choose different colors and widths to use while drawing. If you click Build, you will be prompted for the destination of the executable and it will be built. Running the newly built executable will draw the same graphic on the screen and you will be able to watch it being drawn.
In order to store information about the drawings, I have developed two classes: Curve
and Drawing
. Here are their class diagrams:
The Curve
class carries information about all mouse movements that occur while the mouse button is pressed. The coordinates of all points are stored in a variable of List<Point>
type. _duration
stores the amount of time that the curve was being drawn. _pause
indicates the time that elapsed after the previous curve was drawn. Obviously, it will be equal to zero for the first curve. Drawing
stores the list of curves, as well as the width and height of the screen.
When the user presses the mouse button, a Boolean variable indicating whether the drawing is in progress or not is set to true
. After that, this variable is checked in the MouseMove
event and a line is drawn accordingly:
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
Pen p = new Pen(color, width);
if (isdrawing)
{
using (Graphics gr=this.CreateGraphics())
{
gr.DrawLine(p, prev, e.Location);
cv.Coordinates.Add(prev);
prev = e.Location;
}
}
p.Dispose();
}
cv
is a variable of type Curve
. When the drawing of the current curve finishes, cv
is added to the list of curves in an instance of the Drawing
class.
When the user ends drawing and clicks the Build button, an instance of the Drawing
class is serialized to file. Before serializing, all coordinates are transformed to screen coordinates using the PointToScreen
method. After serialization, an executable file is built using the CSharpCodeProvider
and CompilerParameters
classes. Here is the code snippet:
private bool Compile(string path)
{
bool result;
using (CSharpCodeProvider prov = new CSharpCodeProvider())
{
CompilerParameters param = new CompilerParameters();
string pathtoicon = "";
if (File.Exists(Application.StartupPath + "\\icon.ico"))
{
pathtoicon = Application.StartupPath + "\\icon.ico";
}
param.CompilerOptions =
"/target:winexe" + " " + "/win32icon:" +
"\"" + pathtoicon + "\"";
param.GenerateExecutable = true;
param.GenerateInMemory = false;
param.IncludeDebugInformation = false;
param.EmbeddedResources.Add(
Environment.GetEnvironmentVariable("TEMP")+"\\points.dat");
param.OutputAssembly = path;
param.ReferencedAssemblies.Add("System.dll");
param.ReferencedAssemblies.Add("System.Data.dll");
param.ReferencedAssemblies.Add("System.Deployment.dll");
param.ReferencedAssemblies.Add("System.Drawing.dll");
param.ReferencedAssemblies.Add("System.Windows.Forms.dll");
param.ReferencedAssemblies.Add("System.Xml.dll");
param.TreatWarningsAsErrors = false;
CompilerResults compresults =
prov.CompileAssemblyFromSource(param,
Properties.Resources.Program);
result = compresults.Errors.Count == 0;
File.Delete(Environment.GetEnvironmentVariable("TEMP") +
"\\points.dat");
}
return result;
}
When you launch the generated executable, an object of the Drawing
class is deserialized from the embedded stream and then it is drawn on the desktop. The steps are described below.
If you serialize an object in one assembly and try to deserialize it from another assembly, you will get an error saying that the assembly in which the serialized object was declared cannot be found. You will get the same error even if both assemblies contain the class definition for the specified object. There were two possible ways to solve this problem:
- Use XML Serialization instead of Binary
Or
- Move the class definition to a separate DLL and reference the same DLL from both assemblies
Both of them have disadvantages. XML Serialization is slower than Binary and the output file is larger in size. Defining classes in a separate DLL means that you won't be able to create a stand-alone executable. So, I was stuck with this problem.
After Googling for several hours, I found this website that explains Binary Serialization very deeply: Binary Serialization. One of the concepts discussed was how to deserialize an object to a different type. So, I used this to deserialize my object to the same type, but defined in another assembly. To do that, you have to make your own class inherited from System.Runtime.Serialization.SerializationBinder
and then override the BindToType
method. Here is my implementation:
sealed class typeconvertor : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
Type returntype = null;
if (assemblyName ==
"draw on desktop, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null")
{
assemblyName = Assembly.GetExecutingAssembly().FullName;
returntype =
Type.GetType(String.Format("{0}, {1}",
typeName, assemblyName));
return returntype;
}
if (typeName ==
"System.Collections.Generic.List`1[[Mousemove.Curve,
draw on desktop, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]]")
{
typeName =
typeName.Replace("draw on desktop,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
Assembly.GetExecutingAssembly().FullName);
returntype =
Type.GetType(String.Format("{0}, {1}",
typeName, assemblyName));
return returntype;
}
return returntype;
}
}
After that, you have to set the formatter's Binder
property to this newly created type. The code snippet below will make it clearer:
private Drawing eserialize(Stream input)
{
Drawing temp = null;
try
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Binder = new typeconvertor();
temp = formatter.Deserialize(input) as Drawing;
input.Close();
}
catch (SerializationException ex)
{
MessageBox.Show(ex.ToString());
}
return temp;
}
During deserialization, BindToType
is called several times. When it encounters a type defined in the "draw on desktop" assembly, it substitutes it with the corresponding type from the calling assembly. So, the problem of deserialization is solved.
In order to draw on the screen, we need to retrieve a handle to the device context. To retrieve the handle of the device context, we call the function GetDC
and pass an empty handle. GetDC
is an unmanaged method, so we need to use P/Invoke to be able to call it from our application.
When we have the handle retrieved, we pass it to a static
method of the Graphics
class called Graphics.FromHdc
. This function creates a new Graphics
object that we can use for drawing. Here is a code snippet showing the declaration of the GetDC
function and the creation of a graphics
object.
[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr hWnd);
Graphics gr = Graphics.FromHdc(GetDC(IntPtr.Zero))
Once we have created the graphics
object, we can use it for necessary drawings. So, that's it! I hope you found it interesting and learnt something new.
Deserializing an object into a different type than the one it was serialized into was the most interesting and tricky part in the whole application.
- August 24, 2007 - Initial Release