鼠标绘图






4.75/5 (26投票s)
展示如何在屏幕上绘图的文章

目录
引言
本文介绍的程序允许您绘制不同的图形并将图形嵌入到可执行文件中。当您运行该可执行文件时,它会在桌面上生成完全相同的绘图。更重要的是,您将看到图形被绘制出来。因此,您可以使用鼠标以不同的颜色和不同的宽度绘制或书写内容,构建一个可执行文件并将其发送给他人。当他们运行时,它将重现您绘制的所有内容。
从本文中学到什么
- 如何直接在桌面上绘图
- 如何将对象反序列化为与其序列化时不同的类型
- 如何在运行时编译 C# 代码
背景
如果您熟悉基本的序列化概念,那就太好了。
程序的工作原理
运行程序时,您可以通过按下鼠标按钮并按住它移动来开始绘图。您可以在绘图时选择不同的颜色和宽度。如果您点击“构建”,系统将提示您输入可执行文件的目标位置,然后构建该文件。运行新构建的可执行文件将在屏幕上绘制相同的图形,您将能够看到它被绘制出来。
应用程序背后的代码
用于绘图的类
为了存储有关绘图的信息,我开发了两个类:Curve
和 Drawing
。这是它们的类图。

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
方法将所有坐标转换为屏幕坐标。序列化后,使用 CSharpCodeProvider
和 CompilerParameters
类构建可执行文件。这是代码片段。
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日 - 初始发布