Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Draw with Mouse

0.00/5 (No votes)
17 Oct 2007 1  
An article showing how to draw on screen
Screenshot - Article.jpg

Contents

Introduction

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.

What You Will Learn from This Article

  • 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

Background

It would be nice if you were familiar with basic serialization concepts.

How the Program Works

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.

Code Behind the Application

Classes Used for Drawing

In order to store information about the drawings, I have developed two classes: Curve and Drawing. Here are their class diagrams:

Screenshot - Article1.jpg

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.

Managing the Drawing Process

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.

Serializing and Building the Executable

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 = "";

        //Set executable icon

        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;

        //Add the file to which data was serialized as embedded resource

        param.EmbeddedResources.Add(
            Environment.GetEnvironmentVariable("TEMP")+"\\points.dat");
        param.OutputAssembly = path;

        //Add references

        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;

        //Compile it

        CompilerResults compresults =
            prov.CompileAssemblyFromSource(param,
            Properties.Resources.Program);
        result = compresults.Errors.Count == 0;

        File.Delete(Environment.GetEnvironmentVariable("TEMP") +
            "\\points.dat");
    }
    return result;
}

Recreating the Drawing process

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.

Advanced Binary Serialization: Deserializing an Object Into a Different Type Than the One It was Serialized Into

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();

        //This is the most important part

        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.

Drawing on the Screen

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.

Points of Interest

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.

History

  • August 24, 2007 - Initial Release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here