65.9K
CodeProject 正在变化。 阅读更多。
Home

鼠标绘图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (26投票s)

2007年8月24日

CPOL

5分钟阅读

viewsIcon

132283

downloadIcon

8121

展示如何在屏幕上绘图的文章

Screenshot - Article.jpg

目录

引言

本文介绍的程序允许您绘制不同的图形并将图形嵌入到可执行文件中。当您运行该可执行文件时,它会在桌面上生成完全相同的绘图。更重要的是,您将看到图形被绘制出来。因此,您可以使用鼠标以不同的颜色和不同的宽度绘制或书写内容,构建一个可执行文件并将其发送给他人。当他们运行时,它将重现您绘制的所有内容。

从本文中学到什么

  • 如何直接在桌面上绘图
  • 如何将对象反序列化为与其序列化时不同的类型
  • 如何在运行时编译 C# 代码

背景

如果您熟悉基本的序列化概念,那就太好了。

程序的工作原理

运行程序时,您可以通过按下鼠标按钮并按住它移动来开始绘图。您可以在绘图时选择不同的颜色和宽度。如果您点击“构建”,系统将提示您输入可执行文件的目标位置,然后构建该文件。运行新构建的可执行文件将在屏幕上绘制相同的图形,您将能够看到它被绘制出来。

应用程序背后的代码

用于绘图的类

为了存储有关绘图的信息,我开发了两个类:CurveDrawing。这是它们的类图。

Screenshot - Article1.jpg

Curve 类包含鼠标按钮按下时发生的所有鼠标移动的信息。所有点的坐标都存储在 List<Point> 类型的变量中。_duration 存储绘制曲线所花费的时间。_pause 表示前一条曲线绘制完成后经过的时间。显然,对于第一条曲线,它将等于零。Drawing 类存储曲线列表以及屏幕的宽度和高度。

管理绘图过程

当用户按下鼠标按钮时,一个指示绘图是否正在进行的布尔变量被设置为 true。之后,在 MouseMove 事件中检查此变量,并相应地绘制线条。

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 是一个 Curve 类型的变量。当前曲线绘制完成后,cv 将被添加到 Drawing 类实例的曲线列表中。

序列化和构建可执行文件

当用户完成绘图并单击“构建”按钮时,Drawing 类的一个实例将被序列化到文件中。在序列化之前,使用 PointToScreen 方法将所有坐标转换为屏幕坐标。序列化后,使用 CSharpCodeProviderCompilerParameters 类构建可执行文件。这是代码片段。

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;
}

重现绘图过程

启动生成的执行文件时,Drawing 类的一个对象将从嵌入的流中反序列化,然后将在桌面上绘制。步骤如下。

高级二进制序列化:将对象反序列化为与序列化时不同的类型

如果您在一个程序集中序列化一个对象,然后尝试从另一个程序集中反序列化它,您将收到一个错误,指出找不到声明该序列化对象的程序集。即使两个程序集都包含指定对象的类定义,您也会收到相同的错误。解决此问题的两种可能方法是:

  • 使用 XML 序列化而不是二进制序列化

或者

  • 将类定义移到单独的 DLL 中,并从两个程序集中引用相同的 DLL

两者都有缺点。XML 序列化比二进制序列化慢,并且输出文件更大。在单独的 DLL 中定义类意味着您无法创建独立的执行文件。所以,我被这个问题困住了。

经过几个小时的谷歌搜索,我找到了一个网站,它深入探讨了二进制序列化:二进制序列化。讨论的概念之一是如何将对象反序列化为不同的类型。因此,我使用它将我的对象反序列化为同一类型,但定义在另一个程序集中。要做到这一点,您必须创建自己的类,继承自 System.Runtime.Serialization.SerializationBinder,然后重写 BindToType 方法。这是我的实现。

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;
   }
}

之后,您必须将格式化器的 Binder 属性设置为这个新创建的类型。下面的代码片段将使它更清楚。

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;
}

在反序列化过程中,BindToType 会被调用几次。当它遇到在“在桌面绘图”程序集中定义的类型时,它会用调用程序集中相应的类型替换它。因此,反序列化问题得到了解决。

在屏幕上绘图

为了在屏幕上绘图,我们需要检索设备上下文的句柄。要检索设备上下文的句柄,我们调用 GetDC 函数并传递一个空的句柄。GetDC 是一个非托管方法,所以我们需要使用 P/Invoke 才能在我们的应用程序中调用它。

当检索到句柄后,我们将它传递给 Graphics 类的一个 static 方法,称为 Graphics.FromHdc。此函数创建一个新的 Graphics 对象,我们可以使用它进行绘图。这是一个显示 GetDC 函数声明以及 graphics 对象创建的代码片段。

[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr hWnd);
Graphics gr = Graphics.FromHdc(GetDC(IntPtr.Zero))

一旦创建了 graphics 对象,我们就可以使用它进行必要的绘图。就是这样!我希望您觉得它很有趣并且学到了一些新东西。

关注点

将对象反序列化为与其序列化时不同的类型是整个应用程序中最有趣也是最棘手的部分。

历史

  • 2007年8月24日 - 初始发布
© . All rights reserved.