一、目标与目的:
1 .开发一个小软件,从而可以对上网时间进行监控。当满足预先设置的条件时就将网络连接强行挂断。从而减小自制力差的人过度迷信网络,哈。
2 .很想试试开发 Windows 程序,通过这个小程序的开发来熟悉一下 .NET 下的 WinForm 的开发。
二、软件的功能简介:
1 .检测是否连接到互联网,本软件采用的是每隔 5 秒钟检测一次的设计方式,所以,如果有连接的话,就加上 5 秒钟,否则不加。(暂时还不能检测用校园网网关的连接)
2 .计算当月、当天的上网时间,及总共的上网时间。如果当月或当天上网时间超过了您设置的最大值时,软件将强行中断网络,防止您的网络费用超支。
3 .设置每天、每月上网时间的最大值。 您每月或者每天的上网时间将不能超过此值。
4 .可以设置登陆密码。 只有拥有登陆密码的人才能修改软件的设置信息,这样,没有密码的人的上网时间将在本软件的严格限制下。恩,很适合给小孩子使用啊。
5 .热键功能。 这个功能可以使您快速的显示或者隐藏主窗口,或者您设置了不显示托盘图标的话,只有通过热键来呼叫出程序主窗口了。
三、设计;
<1> 其中 MainForm , SettingForm , LoginForm ,是三个窗体类,他们主要是显示界面,调用一些其他类的方法,在 MainForm 中有很多的序列化和反序列化的地方。
<2> RAS 类主要是包装对 RASAPI 的调用方法的类,这个类中的方法于测试网络连接,断开网络连接有关系。将他们放在一起,可以使层次比较清晰。
<3> NativeWIN32 这个类也是关于 API 的调用,他们大部分都与设置热键有关系。
<4> TimeManager 类里面存储了软件的大部分设置信息,包括,是否启用限制上网,每月限制小时数,以及每个月和每天的上网计时的起始时间等信息。另外他还有两个重要的方法, UpDateStartMonth() 和 UpDateStartDay() 。这两个方法用来更新上网的计时的起始时间。
<5> TotalTimeUsed 类包含了每天、每月以及总的上网时间。这个类并没有什么重要的方法。
<6> Authentic 类和登录软件相关,它里面存贮了登陆的用户名和密码。另外还有一个 IsRightUser() 的方法,这个方法用来验证用户名和密码是否正确。
四、开发过程中的问题及解决方法:
对于 Windows 的程序开发,我以前根本没有参与和实践过,这个程序也很早就很想做。因为没有经验,也只好步步为营,一个功能一个功能的做,所以前期的考虑也不是很多。因为很多东西根本不知道怎么实现才好。下面我就说说我的开发历程和所遇到过的困难。
1 .检测网络连接
这个问题是我最先想解决的,一开始,我想用 Ping 来检测,不过后来证明它的性能太差劲了。后来这个问题是在我解决第二个问题的时候给捎带着解决了。这是 Ping 的代码:
string PingCmd(string Ip) { Process p = new Process(); p.StartInfo.FileName ="cmd.exe"; // 调用的是 CMD.EXE p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardInput = true; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.StartInfo.CreateNoWindow = true; string pingrst; p.Start(); p.StandardInput.WriteLine("ping -n 1 "+ Ip); // ping –n 1 IP p.StandardInput.WriteLine("exit"); string strRst = p.StandardOutput.ReadToEnd(); //取得输出结果 if(strRst.IndexOf("(0% loss)")!=-1) pingrst = " 连接 "; else pingrst =” 无法连接 ”; p.Close(); return pingrst; } |
2 .用编程方式强行中断网络连接
一开始,我以为这个问题不会很难,顶多调用 Windows API 不就完事了。后来我查了一天的资料才搞定。中间还有一次误入歧途,甚是郁闷啊。最后我是在 VCHome 的网站上找到了一点线索。知道了是用 RASAPI32 中的函数。他们分别是 RasEnumConnections (枚举网络连接)、 RasGetConnectStatus (得到连接状态)、 RasHangUp (断开网络连接)。在写平台调用的时候出现了更大的麻烦。因为必须用 .NET 中的类型来对应 API 中使用的类型,尤其是结构,要在代码中从新定义。可能只有参考 MSDN 才能说清楚这件事情。如果你实在定义不出来,也可以参考一下网上别人的定义。 http://www.webtropy.com/ ,这个网站上又很多 API 在 .NET 下的引用方法。
不过,基本的有几点要注意:
<1>. HWND 对应 IntPtr
<2>. 如果想用与 API 不同的函数名称应该在 Dllimport 属性中注明程序入口点
<3>. 所要用的结构要从新定义在工程里,其中结构中的定长数组要用 MarshalAs 属性封送为 UnmanagedType.ByValTStr 类型,还要注明 SizeCount 的大小。另外如果是缓冲区的话,就用 StringBuilder 封送。但也要注明大小。不要忘了大小要加 1 。
这样的话,上面提到的三个函数在 C# 中的原型也就是如下面所示
[DllImport("RASAPI32", SetLastError= true , CharSet=CharSet.Auto)] public static extern int RasEnumConnections( [In, Out] RASCONN[] lprasconn, ref int lpcb, ref int lpcConnections); [DllImport("RASAPI32", SetLastError= true, CharSet=CharSet.Auto)] public static extern int RasGetConnectStatus( IntPtr hrasconn, ref RASCONNSTATUS lprasconnstatus); [DllImport("RASAPI32",SetLastError= true , CharSet=CharSet.Auto)] public static extern int RasHangUp (IntPtr hrasconn); |
下面是一个结构的定义:注意它里面的定长数组的定义。
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Auto)] public struct RASCONN { public int dwSize; public IntPtr hrasconn; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=RAS_MaxEntryName+1)] public string szEntryName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=RAS_MaxDeviceType+1)] public string szDeviceType; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=RAS_MaxDeviceName+1)] public string szDeviceName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_PATH)] public string szPhonebook; public int dwSubEntry; } |
话说回来上次的第一个没解决的问题只要调用以下 RasEnumConnections ,然后处理一下结果就行了。需要什么连接信息都可以从结构中获得。定义了原型以后,我们就可以在想需要他们的地方随意调用了。反正他们也是静态的函数。后面就是 .NET 平台自己去查找 API 函数并将他们载入到内存中。并将参数压入堆栈。这些都是自动的,我们不用去管。不过,平台调用成功与否与原型定义和数据封送有很大的关系。
3. 保存运行时状态(使用 .NET 的序列化功能)
对于保存运行时的状态参数的问题,我的一个直接的想法是保存到文本文件中,事实证明,这种方法对于保存数量较少的状态参量还可以,否则,要进行复杂的字符串操作。后来,我又想到了用比较先进的 XML 技术。因为自己对 XML 并不很熟悉,并且还要写一些 XML 文件。觉得不是很爽。最后突然想到了某次技术交流的时候,师哥们猛吹 .NET 的序列化是如何如何的方便。于是也就决定用序列化了。
.NET 提供的序列化真的很方便,它的步骤很简单。
最简单的办法就是用 [Serialization] 来标注你要序列化的对象的类。使用 NonSerialized 属性来标注你不想序列化的变量。
[Serializable] public class TotalTimeUsed { private int useNetTime; private int useNetDayTime; private int useNetTotalTime; public TotalTimeUsed() { } } |
然后用如下操作来将其序列化为二进制文件。
IFormatter formatter = new BinaryFormatter(); Stream stream = new FileStream("use.sav", FileMode.OpenOrCreate,FileAccess.ReadWrite, FileShare.None); formatter.Serialize(stream, this .timeUsedObject); stream.Close() ; |
对于反序列化也非常简单,只要调用,相应的 Deserialize 方法就行了。 但是要注意的是,反序列化, 出来的对象是 Object 类型的,要把它强制类型转化成实际的类型。
4. 在 .NET 平台下使用资源
使用资源的好处很多,包括实现国际化,资源不用编译等等。
在 .NET 下使用资源也非常的方便,我觉得最简的方法莫过于使用 ResEditor 工具,不用怕,它是一个可视化的工具,而且是 IDE 自带的,它在 SDK\v1.1\Samples\Tutorials\resourcesandlocalization\r
Eseditor 这个目录下面,需要先运行 bat 文件,然后就会出现 .exe 的可执行文件。不过,我运行完了以后只是出现了两个 .cs 文件,给他们编译完了就会出现想要的 ResEditor.ext ,好了,将它加入到 VS.NET 中的 Tools 中吧。由于每个 Form 窗体都对应一个 .resx 文件,这就是我们要找的资源文件,我只要用刚才生成的工具打开这个文件就可以对资源文件进行添加,删除,重命名等。可以说是非常的智能化,下面就是 ResEditor 的截图。
这样,通过这个工具,把你想要的资源都加入好了以后,就可以保存了。
可是怎么使用呢?
是这样的,如果要使用资源,就要用到 ResourceManager 类,在要用资源的地方新建一个 ResourceManager 类,例如: ResourceManager resources = new System.Resources.ResourceManager( typeof (MainForm));
MainForm 就是所在的窗体类的类名,这样新建出的 ResourceManager 类就可以管理 MainForm 的默认资源文件了,即: MainForm.resx 。这样第一步就完成了,下面,就是调用 GetObject 方法来取得资源了。例如: this .NotifyIcon1.Icon = ((System.Drawing.Icon)(resources.GetObject("NotifyIcon1.Icon")));
GetObject 的参数就是要取得的资源的名字。
5. 对于 MDI 编程
只有一个问题困扰了我一会,主要怎么设置才能让弹出对话框的时候,主窗口处于不可用状态,其实只要调用不同的方法就行了。对于这个问题只要调用 ShowDialog 就行了,而不是调用 Show 。当然也不用设置父窗口。
6. 程序自动运行
这个问题并不难,最简单的方法就是写入注册表到 HKEY_LOCALMACHINE\SOFTWARE\MICROSOFT\WINDOWS\CurrentVersion\Run 就可以了。
注册表的读取和写入相对于文件要简单的多。先给出我的代码。
public void ReadWriteReg() { Assembly aa = Assembly.GetExecutingAssembly() ; // 获得当前运行的程序集 string location = aa.Location ; // 获得当前程序集的物理地址 RegistryKey Hklm = Registry.LocalMachine ; // 读取 LocalMachine 键 RegistryKey HkSoftware = Hklm.OpenSubKey ("SoftWare") ; // 读取 Software 子键 RegistryKey HkMicrosoft = HkSoftware.OpenSubKey ("Microsoft") ; RegistryKey HkWindows = HkMicrosoft.OpenSubKey ("Windows") ; RegistryKey HkCurrent = HkWindows.OpenSubKey ("CurrentVersion"); RegistryKey HkRun = HkCurrent.OpenSubKey ("Run", true ) ; // 打开 Run 子键,并且它是可写入的 HkRun.SetValue("LimitNetUseTime",location); // 写入键值 HkRun.Close() ; HkCurrent.Close() ; HkWindows.Close() ; HkMicrosoft.Close() ; HkSoftware.Close() ; Hklm.Close() ; } |
基本上就需要两个类, RegistryKey 和 Registry ,其中 Registry 主要用来获取注册表的根,通过它的属性就可以获得你想要的根,然后就是要层层深入的读取。在你想要写入的键值的地方调用 SetValue 就行了。要说明的是 OpenSubKey 有两个参数的时候,第二个参数代表是否可以写入。另外操作完后,要调用 Close() 。
7. 编写托盘程序
鉴于要编写的程序的性质,所以要把它编写为托盘程序,也就是,它只在托盘区有个小图标的那种程序。其实要使用到一个控件,它就是 NotifyIcon ,还可以添加 ContextMenu 控件来增加右键菜单,然后给菜单加上事件处理。
另外再重写 OnClosing 函数。
protected override void OnClosing ( System.ComponentModel.CancelEventArgs e ) { SaveUse(); // 保存使用时间 e.Cancel = true ; this .SetSettingEnable( false ) ; this .SetMainTitle(" 限制--您未登陆, 请您输入密码 ") ; Hide(); if OpenWindow = false ; } |
这样的话,还可以实现 FlashGet 的那种效果,即:点击窗口上的 X 时,并不关闭程序的效果。
五、总结
这个程序现在也只是停留在基本上能用的阶段,因为时间并不多,也只能暂时告一段落了,等有时间了再把界面改改,再加上流量统计,按月份或者按天生成报表,如果要是能把进程插入到其他系统进程中,防止用户中止进程就好了。要是给家长用,再给加上键盘记录,屏幕界图。啊 … .. 怎么越说越像木马了,太可怕了。我可是好孩子呀,呵呵 :)
虽然只有几天的时间,几个简单的类,但是做这个小程序却让我收获很大。让我真真切切的感觉到 .NET 平台对开发程序的种种优良的特性。如果你也觉得哪个软件用的不爽,也写一个啦。在最后,我也发现有很多我写的代码都是相同的,于是也进行了一次小小的重构,比起一开始写的代码要好很多。还有,在我的开发过程中,有两个工具对我来说是不可或缺的,那就是 Google 和 MSDN ,用他们几乎可以找到任何你想要的东西,关键看你能否找的出来。就连黑客们都用 Google 来寻找他们要进攻的对象,实在太厉害啊,所以啊,有什么问题―――先放“狗”去搜吧。