多平台统一管理软件接口,如何实现多平台统一管理软件接口
353
2022-06-22
本章说明
在前面章节中,笔者使用了反射和动态编译技术实现了快速ORM框架,在本章中笔者将继续使用这些技术来实现一个VB.NET的脚本引擎,使得人们在开发中能实现类似MS Office那样实现VBA宏的功能。本章配套的演示程序为/Files/xdesigner/MyVBAScript.zip.
脚本的原理
脚本,也可称为宏,是一种应用系统二次开发的技术,它能在应用系统提供的一个容器环境中运行某种编程语言,这种编程语言代码调用应用系统提供的编程接口,使得应用系统暂时“灵魂附体”,无需用户干预作而执行一些自动的操作,此时应用系统称为“宿主”。
脚本也采用多种编程语言,比如JavaScript语言,VBScript语言或者其他的,若采用VB则称为VB脚本。
下图为脚本原理图
脚本语言大多是动态语言,所谓动态语言就是程序代码在编写时已经假设操作的对象的类型,成员属性或方法的信息,而编译器不会进行这方面的检查。C#不是动态语言,是静态语言,因为它在编译时会严格的检查代码操作的对象的类型,成员信息,稍有不对则会报编译错误。VB.NET源自VB,是动态语言,它在编译时不会严格的检查对象的类型及其成员信息,执行后期绑定,而是在运行时检查,若运行时发现对象类型和成员信息错误,则会报运行时错误。脚本技术应当非常灵活和自由,袁某人觉得此时使用C#这种静态语言不是明智之举,而应当使用类似VB.NET这样的动态语言。
而常规的软件开发而生成的软件大多是事先编译好的,和应用系统是独立的,软件是调用应用系统的功能而不是应用系统的一部分。软件代码修改会导致软件的重新编译和部署,应用系统必须提供DLL格式的程序集文件。
微软的很多软件产品有有VBA的功能,比如MS Office,甚至VS.NET集成开发环境也有VBA宏的功能。脚本提供给应用系统二次开发的能力,而且这种二次开发能力简单灵活,部署方便。
在应用方面脚本技术带来的最大好处就是简单灵活,部署方便。脚本代码以纯文本的格式进行存储,修改方便,而且脚本修改后,应用系统无需重新启动而能立即使用新的脚本,脚本代码中能实现比较复制的逻辑控制,能响应应用系统的事件,能一定程度上扩展应用系统的功能,这有点类似数据库中的存储过程。
VB.NET脚本原理
VB.NET脚本就是采用VB.NET语法的脚本。VS.NET集成开发环境提供的宏也是采用VB.NET语法。微软.NET框架提供了一个脚本引擎,那就是在程序集microsoft.visualbasic.vsa.dll中的类型Microsfot.VisualBasic.Vsa.VsaEngine,该类型在微软.NET框架1.1和2.0中都有,使用起来不算容易,而且在微软.NET框架2.0中VsaEngine类型标记为“已过时”。在此笔者不使用VsaEngine类型来实现VB.NET脚本,而是使用动态编译技术来实现脚本引擎。
使用动态编译技术实现VB.NET脚本引擎的原理是,程序将用户的脚本代码字符串进行一些预处理,然后调用Microsoft.VisualBasic.VBCodeProvider类型的CompileAssemblyFromSource函数进行动态编译,生成一个临时的程序集对象,使用反射技术获得程序集中的脚本代码生成的方法,主程序将按照名称来调用这些脚本代码生成的方法。若用户修改了脚本代码,则这个过程重复一次。
VB.NET脚本引擎设计
这里笔者将用倒推法来设计VB.NET脚本引擎,也就是从预期的最终使用结果来反过来设计脚本引擎。
主程序将按照名称来调用脚本方法,很显然VB.NET代码编译生成的是一个.NET程序集类库,为了方便起见,笔者将所有的VB.NET脚本方法集中到一个VB.NET脚本类型。笔者将脚本方法定义为静态方法,主要有两个好处,首先脚本引擎不必生成对象实例,其次能避免由于没有生成对象实例而导致的空引用错误,这样能减少脚本引擎的工作量。
在VB.NET语法中,可以使用代码块“public shared sub SubName()”来定义一个静态过程,但笔者不能要求用户在编写VB.NET脚本代码时使用“public shared sub SubName()”的VB.NET语法,而只能使用“sub SubName()”这样比较简单的语法。同样用户在脚本中定义全局变量时不能使用“private shared VarName as TypeName”的语法,而是简单的使用“dim VarName as TypeName”的语法。这时笔者可以使用VB.NET语法的模块的概念。在VB.NET语法中,将代码块“Module ModuleName ……. End Module”中的所有的代码编译为静态的。比如把“sub SubName”编译成“public shared sub SubName()”,把“dim VarName as TypeName”编译为“public shared VarName as TypeName”。这样借助VB.NET模块的概念就能解决了这个问题。
在一些脚本中笔者经常可以看见类似“window.left”或者“document.location”的方式来使用全局对象,若笔者在VB.NET中直接使用“window.left”之类的代码必然报“window”对象或者变量找不到的编译错误。
“document”或者“window”等全局对象是映射到文档或者主窗体等实例对象的,因此它们的成员不能定义成静态,为了能实现在脚本代码中直接使用类似“window.left”的方法来直接使用全局对象,笔者又得使用VB.NET的一个语法特性。在Microsoft.VisualBasic.dll中有一个公开的特性类型“Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute”,该特性是隐藏的,可能不是微软推荐使用,但在微软.NET框架1.1和2.0都有这个特性类型,功能也是一样的。对于一般的编程该特性是用不着的,它可附加在某个类型上,VB.NET编译器会认为附加了该特性的类型的静态属性值就是全局对象。比如笔者定义了一个GlobalObject类型,附加了StandardModuleAttribute特性,它有一个名为Document的静态属性,在对于脚本中的“document.Location”代码块,VB.NET编译器会针对“document”标识符检索所有附加了StandardModuleAttribute的类型的静态属性,最后命中GlobalObject类型,于是会自动扩展为“GlobalObject.Document.Location”的代码。这个过程是在编译时进行的,在实际运行中不再需要进行这样的查找,这样的语法特点是C#所没有的。上述的这些特点使得VB.NET语法更适合作为脚本的语法。
类似全局对象,在VB.NET语法中具有全局函数的功能,比如对于Asc函数,它实际上是类型Microsoft.VisualBasic.Strings的一个静态成员函数,但在VB.NET中可以在任何时候任何地方直接使用,VB.NET编译器会将代码中的Asc函数自动扩展为“Microsoft.VisualBasic.Strings.Asc”。这个过程是在编译时进行的,而运行时不再需要这样的扩展。
.NET框架自带VB.NET编译器,它就是在.NET框架安装目录下的vbc.exe,在笔者的系统中VB.NET编译器的路径是“C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"vbc.exe”,参考MSDN中关于VB.NET编译的命令行的说明,它支持一个名为“imports”的命令行参数指令。比如可以这样调用VB.NET编译器“vbc.exe /imports:Microsoft.VisualBasic,system,system.drawing 其他参数”,该参数的功能是从指定的程序集导入名称空间。在VB.NET编译器命令行中使用imports指令和在VB.NET代码中使用Imports指令是不一样的。在源代码中使用Imports指令是用于减少代码编写量,而在命令行中使用imports指令是启动指定名称空间下的全局对象和全局函数,若一个类型附加了StandardModuleAttribute特性,而且定义了一些静态函数和属性,但并没有在编译器命令行中导入带类型所在的名称空间,则VB.NET编译器不会感知到该类型中定义的全局对象和全局函数,因此在编写VB.NET代码时必须使用“类型名称.静态属性或函数的名称”的方式来调用全局对象和全局函数。比如若没有在VB.NET编译器的命令行参数中使用“/imports:Microsoft.VisualBasic”参数,则Asc函数不再是全局函数,若在代码中直接使用Asc函数则必然报编译错误,而必须使用“Microsoft.VisualBasic.Strings.Asc”的方式来使用,即使源代码中使用了“Imports Microsoft.VisualBasic”,也只能用“Strings.Asc”的方式来使用函数。
如上所述,借助于StandardModuleAttribute特性和编译器命令行参数imports,笔者就可以实现VB.NET的全局对象和全局函数了。
根据上述说明,笔者设计如下的参与动态编译的VB.NET脚本代码的结构
Option Strict Off
Imports System
Imports Microsoft.VisualBasic
Namespace NameSpaceXVBAScriptEngien
Module mdlXVBAScriptEngine
sub 脚本方法1()
'VB.NET代码
end sub
sub 脚本方法2()
'VB.NET代码
end sub
End Module
End Namespace
在脚本引擎自动添加的代码中使用了Imports语句引入的名称空间,默认添加了System和Microsoft.VisualBasic两个名称空间,为了方便使用,可以让用户添加其他的名称空间,比如脚本代码中大量使用了System.Drawing名称空间,则可以使用Imports语句导入System.Drawing名称空间来减少脚本代码量。
软件开发
笔者新建一个XVBAEngine类型,该类型实现了脚本引擎的功能。脚本引擎包含了参数控制属性,代码生成器,动态编译,分析和调用临时程序集等几个子功能。
参数控制属性
笔者为脚本引擎类型定义了几个属性用于保存脚本引擎运行所必备的基础数据。这些属性中最重要的属性就是用户设置的原始脚本代码文本。定义该属性的代码如下
///
/// 脚本代码改变标记
///
private bool bolScriptModified = true;
///
/// 原始的VBA脚本文本
///
private string strScriptText = null;
///
/// 原始的VBA脚本文本
///
public string ScriptText
{
get
{
return strScriptText;
}
set
{
if (strScriptText != value)
{
bolScriptModified = true;
strScriptText = value;
}
}
}
在这里ScriptText属性表示用户设置的原始的VBA脚本代码,实际参与动态编译的脚本代码和原始设置的原始的VBA脚本代码是不一致的。当用户修改了脚本代码文本,则会设置bolScriptModified变量的值,脚本引擎运行脚本方法时会检查这个变量的值来判断是否需要重新动态编译操作。
此外袁某人还定义了其他的一些控制脚本引擎的属性,其定义的代码如下
private bool bolEnabled = true;
///
/// 对象是否可用
///
public bool Enabled
{
get
{
return bolEnabled;
}
set
{
bolEnabled = value;
}
}
private bool bolOutputDebug = true;
///
/// 脚本在运行过程中可否输出调试信息
///
public bool OutputDebug
{
get
{
return bolOutputDebug;
}
set
{
bolOutputDebug = value;
}
}
编译脚本
笔者为脚本引擎编写了Compile函数用于编辑脚本。编译脚本的过程大体分为生成脚本代码文本、编译脚本编译、分析脚本程序集三个步骤。
生成脚本代码文本
根据上述对运行时脚本的设计,用户可以导入其他的名称空间,于是脚本引擎定义了SourceImports属性来自定义导入的名称空间,定义该属性的代码如下
///
/// 源代码中使用的名称空间导入
///
private StringCollection mySourceImports = new StringCollection();
///
/// 源代码中使用的名称空间导入
///
public StringCollection SourceImports
{
get
{
return mySourceImports;
}
}
在脚本引擎的初始化过程中,程序会默认添加上System和Microsoft.VisualBasic两个名称空间。随后程序使用以下代码来生成实际参与编辑的脚本代码文本
// 生成编译用的完整的VB源代码
string ModuleName = "mdlXVBAScriptEngine";
string nsName = "NameSpaceXVBAScriptEngien";
System.Text.StringBuilder mySource = new System.Text.StringBuilder();
mySource.Append("Option Strict Off");
foreach (string import in this.mySourceImports)
{
mySource.Append(""r"nImports " + import);
}
mySource.Append(""r"nNamespace " + nsName);
mySource.Append(""r"nModule " + ModuleName);
mySource.Append(""r"n");
mySource.Append(this.strScriptText);
mySource.Append(""r"nEnd Module");
mySource.Append(""r"nEnd Namespace");
string strRuntimeSource = mySource.ToString();
编译脚本
程序生成完整的VB.NET脚本代码文本后就可以编译了,为了提高效率,这里袁某定义了一个静态myAssemblies的哈希列表变量,定义该变量的代码如下
///
/// 所有缓存的程序集
///
private static Hashtable myAssemblies = new Hashtable();
该列表缓存了以前编辑生成的程序集,键值就是脚本文本,键值就是程序集。若缓存区中没有找到以前缓存的程序集那脚本引擎就可以调用VB.NET编译器编辑脚本了。
为了丰富脚本引擎的开发接口,笔者使用以下代码定义了ReferencedAssemblies属性。
///
/// VB.NET编译器参数
///
private CompilerParameters myCompilerParameters = new CompilerParameters();
///
/// 引用的名称列表
///
public StringCollection ReferencedAssemblies
{
get
{
return myCompilerParameters.ReferencedAssemblies;
}
}
ReferencedAssemblies保存了编辑脚本时使用的程序集,在初始化脚本引擎时,系统已经默认向该列表添加了mscorlib.dll、System.dll、System.Data.dll、System.Xml.dll、System.Drawing.dll、System.Windows.Forms.dll、Microsoft.VisualBasic.dll等.NET框架标准程序集,用户可以使用该属性添加第三方程序集来增强脚本引擎的功能。
在前面的说明中,为了实现全局对象和全局函数,需要在VB.NET编译器的命令上中使用imports指令导入全局对象和全局函数所在的名称空间,为此笔者定义了一个VBCompilerImports的属性来保存这些名称空间,定义该属性的代码如下
///
/// VB编译器使用的名称空间导入
///
private StringCollection myVBCompilerImports = new StringCollection();
///
/// VB编译器使用的名称空间导入
///
public StringCollection VBCompilerImports
{
get
{
return myVBCompilerImports;
}
}
在初始化脚本引擎时程序会在VBCompilerImports列表中添加默认的名称空间Microsoft.VisualBasic。
// 检查程序集缓存区
myAssembly = (System.Reflection.Assembly)myAssemblies[strRuntimeSource];
if (myAssembly == null)
{
// 设置编译参数
this.myCompilerParameters.GenerateExecutable = false;
this.myCompilerParameters.GenerateInMemory = true;
this.myCompilerParameters.IncludeDebugInformation = true;
if (this.myVBCompilerImports.Count > 0)
{
// 添加 imports 指令
System.Text.StringBuilder opt = new System.Text.StringBuilder();
foreach (string import in this.myVBCompilerImports)
{
if (opt.Length > 0)
{
opt.Append(",");
}
opt.Append(import.Trim());
}
opt.Insert(0, " /imports:");
for (int iCount = 0; iCount < this.myVBCompilerImports.Count; iCount++)
{
this.myCompilerParameters.CompilerOptions = opt.ToString();
}
}//if
if (this.bolOutputDebug)
{
// 输出调试信息
System.Diagnostics.Debug.WriteLine(" Compile VBA.NET script "r"n" + strRuntimeSource);
foreach (string dll in this.myCompilerParameters.ReferencedAssemblies)
{
System.Diagnostics.Debug.WriteLine("Reference:" + dll);
}
}
// 对VB.NET代码进行编译
Microsoft.VisualBasic.VBCodeProvider provider = new Microsoft.VisualBasic.VBCodeProvider();
#if DOTNET11
// 这段代码用于微软.NET1.1
ICodeCompiler compiler = provider.CreateCompiler();
CompilerResults result = compiler.CompileAssemblyFromSource(
this.myCompilerParameters,
strRuntimeSource );
#else
// 这段代码用于微软.NET2.0或更高版本
CompilerResults result = provider.CompileAssemblyFromSource(
this.myCompilerParameters,
strRuntimeSource);
#endif
// 获得编译器控制台输出文本
System.Text.StringBuilder myOutput = new System.Text.StringBuilder();
foreach (string line in result.Output)
{
myOutput.Append(""r"n" + line);
}
this.strCompilerOutput = myOutput.ToString();
if (this.bolOutputDebug)
{
// 输出编译结果
if (this.strCompilerOutput.Length > 0)
{
System.Diagnostics.Debug.WriteLine("VBAScript Compile result" + strCompilerOutput);
}
}
provider.Dispose();
if (result.Errors.HasErrors == false)
{
// 若没有发生编译错误则获得编译所得的程序集
this.myAssembly = result.CompiledAssembly;
}
if (myAssembly != null)
{
// 将程序集缓存到程序集缓存区中
myAssemblies[strRuntimeSource] = myAssembly;
}
}
在这段代码中,首先程序设置编译器的参数,并为VB编译器添加引用的程序集信息,VB.NET编译器有个名为imports的命令行参数用于指定全局名称空间。用法为“/imports:名称空间1,名称空间2”,在编译器命令行中使用imports参数和在代码文本中使用imports语句是有所不同的。
然后程序创建一个VBCodeProvider对象开始编译脚本,对于微软.NET框架1.1和2.0其操作过程是有区别的。对微软.NET1.1还得调用provider的CreateCompilter函数创建一个IcodeCompiler对象,然后调用它的CompileAssemblyFromSource来编译脚本,而对于微软.NET框架2.0则是直接调用provider的CompileAssemblyFromSource来编译脚本的。
编译器编译后返回一个CompilerResults的对象表示编译结果,若发生编译错误程序就输出编译错误信息。若编译成功则程序使用编译结果的CompileAssembly属性获得编辑脚本代码生成的临时程序集对象了。然后把程序集对象缓存到myAssemblies列表中。
分析临时程序集
调用编译器编译脚本代码后成功的生成临时程序集后,脚本引擎需要分析这个程序集,获得所有的可用的脚本方法,其分析代码为
if (this.myAssembly != null)
{
// 检索脚本中定义的类型
Type ModuleType = myAssembly.GetType(nsName + "." + ModuleName);
if (ModuleType != null)
{
System.Reflection.MethodInfo[] ms = ModuleType.GetMethods(
System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Static);
foreach (System.Reflection.MethodInfo m in ms)
{
// 遍历类型中所有的静态方法
// 对每个方法创建一个脚本方法信息对象并添加到脚本方法列表中。
ScriptMethodInfo info = new ScriptMethodInfo();
info.MethodName = m.Name;
info.MethodObject = m;
info.ModuleName = ModuleType.Name;
info.ReturnType = m.ReturnType;
this.myScriptMethods.Add(info);
if (this.bolOutputDebug)
{
// 输出调试信息
System.Diagnostics.Debug.WriteLine("Get vbs method """ + m.Name + """");
}
}//foreach
bolResult = true;
}//if
}//if
在这段代码中,程序首先获得脚本模块的类型,在这里类型全名为“NameSpaceXVBAScriptEngien. mdlXVBAScriptEngine”,然后使用反射获得该类型中所有的公开或未公开的静态成员方法对象,对于其中的每一个方法创建一个ScriptMethodInfo类型的脚本方法信息对象来保存这个方法的一些信息,将这些信息保存到myScriptMethods列表中供以后调用。
笔者配套定义了ScriptMethodInfo类型和myScriptMethods列表,定义它们的代码如下
///
/// 所有脚本方法的信息列表
///
private ArrayList myScriptMethods = new ArrayList();
///
/// 脚本方法信息
///
private class ScriptMethodInfo
{
///
/// 模块名称
///
public string ModuleName = null;
///
/// 方法名称
///
public string MethodName = null;
///
/// 方法对象
///
public System.Reflection.MethodInfo MethodObject = null;
///
/// 方法返回值
///
public System.Type ReturnType = null;
///
/// 指向该方法的委托
///
public System.Delegate MethodDelegate = null;
}
使用脚本方法信息列表,脚本引擎调用脚本方法时就不需要使用反射查找脚本方法了,只需要在脚本方法信息列表中快速的查找和调用。
调用脚本
脚本引擎前期完成的大量的工作就是为了最后能调用脚本,为此笔者定义了、Execute函数用于调用指定名称的脚本方法。定义该函数的代码如下
///
/// 执行脚本方法
///
/// 方法名称
/// 参数
/// 若发生错误是否触发异常
///
public object Execute(string MethodName, object[] Parameters, bool ThrowException)
{
// 检查脚本引擎状态
if (CheckReady() == false)
{
return null;
}
if (ThrowException)
{
// 若发生错误则抛出异常,则检查参数
if (MethodName == null)
{
throw new ArgumentNullException("MethodName");
}
MethodName = MethodName.Trim();
if (MethodName.Length == 0)
{
throw new ArgumentException("MethodName");
}
if (this.myScriptMethods.Count > 0)
{
foreach (ScriptMethodInfo info in this.myScriptMethods)
{
// 遍历所有的脚本方法信息,不区分大小写的找到指定名称的脚本方法
if (string.Compare(info.MethodName, MethodName, true) == 0)
{
object result = null;
if (info.MethodDelegate != null)
{
// 若有委托则执行委托
result = info.MethodDelegate.DynamicInvoke(Parameters);
}
else
{
// 若没有委托则直接动态执行方法
result = info.MethodObject.Invoke(null, Parameters);
}
// 返回脚本方法返回值
return result;
}//if
}//foreach
}//if
}
else
{
// 若发生错误则不抛出异常,安静的退出
// 检查参数
if (MethodName == null)
{
return null;
}
MethodName = MethodName.Trim();
if (MethodName.Length == 0)
{
return null;
}
if (this.myScriptMethods.Count > 0)
{
foreach (ScriptMethodInfo info in this.myScriptMethods)
{
// 遍历所有的脚本方法信息,不区分大小写的找到指定名称的脚本方法
if (string.Compare(info.MethodName, MethodName, true) == 0)
{
try
{
// 执行脚本方法
object result = info.MethodObject.Invoke(null, Parameters);
// 返回脚本方法返回值
return result;
}
catch (Exception ext)
{
// 若发生错误则输出调试信息
System.Console.WriteLine("VBA:" + MethodName + ":" + ext.Message);
}
return null;
}//if
}//foreach
}//if
}//else
return null;
}//public object Execute
这里函数参数为要调用的脚本方法的名称,不区分大小写,调用脚本使用的参数列表,还有控制是否抛出异常的参数。在函数里面,程序遍历myScriptMethods列表中所有以前找到的脚本方法的信息,查找指定名称的脚本方法,若找到则使用脚本方法的Invoke函数执行脚本方法,如此陈旭就能调用脚本了。
为了丰富脚本引擎的编程接口,笔者还定义了HasMethod函数来判断是否存在指定名称的脚本方法,定义了ExecuteSub函数来安全的不抛出异常的调用脚本方法。
Window全局对象
在很多脚本中存在一个名为“window”的全局对象,该对象大多用于和用户界面互换,并映射到应用系统主窗体。在这里笔者仿造HTML的javascript脚本的window全局对象来构造出自己的window全局对象。
参考javascript中的window全局对象,对笔者有参考意义的类型成员主要分为映射到屏幕大小或者主窗体的位置大小的属性,还有延时调用和定时调用的方法,还有显示消息框或输入框的方法。
笔者建立一个XVBAWindowObject类型作为Window全局对象的类型。
成员属性
笔者首先定义一个UserInteractive属性,该属性指定应用系统是否能和用户桌面交互。定义该属性的代码如下
protected bool bolUserInteractive = true;
///
/// 是否允许和用户交互,也就是是否显示用户界面
///
///
public bool UserInteractive
{
get { return bolUserInteractive; }
set { bolUserInteractive = value; }
}
一些应用系统,包括ASP.NET和Windows Service,它是不能和用户交互的,不能有图形化用户界面,不能调用MessageBox函数,不能使用.NET类库中System.Widnows.Forms名称空间下的大部分功能,若强行调用则会出现程序错误。这个脚本引擎设计目标是可以运行在任何程序类型中的,包括WinForm,命令行模式,ASP.NET和Windows Service。因此在这里笔者定义了UserInteractive属性用于关闭window全局对象的某些和用户互换相关的功能,比如显示消息框,延时调用和定时调用等等,主动关闭这些功能对应用系统的影响是不大的。
笔者还定义了其他的一些属性,其定义的代码如下
protected string strSystemName = "应用程序";
///
/// 系统名称
///
public string SystemName
{
get
{
return strSystemName;
}
set
{
strSystemName = value;
}
}
protected XVBAEngine myEngine = null;
///
/// 脚本引擎对象
///
public XVBAEngine Engine
{
get { return myEngine; }
}
protected System.Windows.Forms.IWin32Window myParentWindow = null;
///
/// 父窗体对象
///
public System.Windows.Forms.IWin32Window ParentWindow
{
get { return myParentWindow; }
set { myParentWindow = value; }
}
///
/// 屏幕宽度
///
public int ScreenWidth
{
get
{
if (bolUserInteractive)
{
return System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
}
else
{
return 0;
}
}
}
///
/// 屏幕高度
///
public int ScreenHeight
{
get
{
if (bolUserInteractive)
{
return System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;
}
else
{
return 0;
}
}
}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~