winform程序相对web程序而言,功能更强大,编程更方便,但软件更新却相当麻烦,要到客户端一台一台地升级,本文结合实际情况,通过软件实现自动升级,弥补了这一缺陷,有较好的参考价值。
由于程序在运行时不能用新的版本覆盖自己,因此,我们将登录窗口单独做成一个可执行文件,用户登录时,从网上检测是否有新的主程序,如果有,则从后台下载并覆盖老的版本,用户输入正确的用户名和密码后,通过参数将必要的信息(如用户名、密码等)传递给主程序,实现登录,我们还是以实际例子来说明。
创建一个项目,不妨取名为mainpro,作为主程序,切换到代码窗口,看到如下一段代码:
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[stathread]
static void main()
{
application.run(new form1());
}
为了接收参数,我们添加两个静态变量m_username和m_password用于存放用户名和密码,并修改main函数为:
private static string m_username,m_password;
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[stathread]
static void main(string[] args)
{
if(args.length==2)//有参数输入,你还可以根据实际情况传入更多参数
{
//记录下用户名和密码,供软件使用
m_username=args[0];
m_password=args[1];
application.run(new form1());
}
else
{
messagebox.show("不能从这里启动");
}
}
为了显示登录是否正确,load事件的代码修改为:
private void form1_load(object sender, system.eventargs e)
{
string msg=string.format("用户名:{0},密码:{1}",m_username,m_password);
messagebox.show(msg);
}
这样,我们的示例主程序就完成了,只有加入参数才能运行该主程序,例如我们在dos窗口中用“mainpro user pass”来启动该软件。
由于本项目涉及到不止一个程序,为保证运行正确,需要将编译后的可执行文件放到同一个文件夹,尽管我们可以编译后再将文件复制到同一个文件夹中,但每次都手工复制比较费事,这里采取一个简单的办法。先在硬盘中创建一个文件夹,如d:/output,选择菜单“项目”→“属性”,会弹出一个对话框,在配置(c)后面选择“所有配置”,选择配置属性的生成项,在输出路径中输入“d:/output”(如下图),再编译时你就发现,输出的可执行文件乖乖地跑到d:/output下面了。
接下来做一个上传工具,目的是将最新版本上传到服务器上,为简单,我们这里使用access数据库,当然,在网络版中可以使用sql server,原理完全一样。
在d:/output下新建一个access数据库,取名为mydatabase.mdb吧,新建两个表,一个为操作员,用来存放操作员的姓名和密码,另外一个为版本,用来存放主程序的最新版本,两个表的结构如下:
操作员表 | 版本表 |
字段名 | 类型 | 用途 | 字段名 | 类型 | 用途 |
序号 | 长整型 | 主键 | 序号 | 长整型 | 主键 |
姓名 | 字符 | 用户名 | 版本号 | 长整型 | 存放当前版本 |
文件名称 | 字符 | 本记录对应的文件名 |
密码 | 字符 | 密码 | 文件内容 | ole 对象,sql 中为image | 存放文件的具体内容 |
我们手工输入一些用户名和密码,如下:
不要关闭刚才的主程序,直接选择菜单“文件”→“添加项目”→“新建项目”,输入项目名称为“upload”,如下图:
点“确定”,同样,配置输出路径为d:/output。
在窗口上放入三个按钮(浏览(btnbrow)、确定(btnok)和取消(btncancel))、两个文本框(txtfilename,txtversion)和相应的文字说明,如下图:
在“解决方案资源管理器”窗口中,选择“upload”项目,单击鼠标右键,选择“设为启动项目”,就可以运行该程序了。
添加浏览按钮的响应代码如下:
private void btnbrow_click(object sender, system.eventargs e)
{
openfiledialog myform=new openfiledialog();
myform.filter="应用程序(*.exe)|*.exe|所有程序(*.*)|*.*";
if(myform.showdialog()==dialogresult.ok)
{
this.txtfilename.text=myform.filename;
}
}
该按钮的作用是得到要上传文件的文件名称(实际应用中,还可以根据得到的文件名,从数据库中得到相对应文件的最高版本号,自动填入的版本号文本框中供输入新版本号时参考)。
添加取消按钮响应代码,目的是关闭窗口:
private void btncancel_click(object sender, system.eventargs e)
{
this.close();
}
添加两个引用:
using system.data.oledb;
using system.io;
再添加两个变量:
private dataset m_dataset=new dataset();
private string m_tablename="版本";
下面的函数去掉文件名中的路径:
/// <summary>
/// 从一个含有路径的文件名中分离出文件名
/// </summary>
/// <param name="p_path">包含路径的文件名</param>
/// <returns>去掉路径的文件名</returns>
private string getfilenamefrompath(string p_path)
{
string strresult="";
int nstart=p_path.lastindexof("//");
if(nstart>0)
{
strresult=p_path.substring(nstart+1,p_path.length-nstart-1);
}
return strresult;
}
添加确定按钮响应代码(含注释):
private void btnok_click(object sender, system.eventargs e)
{
//检查版本号是否合法
try
{
decimal.parse(this.txtversion.text);
}
catch
{
messagebox.show("无效的版本号!");
this.txtversion.focus();
this.txtversion.selectall();
return;
}
if(this.txtfilename.text.trim().length>0)
{
//检查文件是否存在
if(!file.exists(this.txtfilename.text.trim()))
{
messagebox.show("文件不存在!");
return;
}
//连接数据库
string strconnection="provider = microsoft.jet.oledb.4.0 ;jet oledb:database password=;data source ="+
application.startuppath.tostring().trim()+"//mydatabase.mdb" ;
oledbconnection myconnect=new oledbconnection(strconnection);
oledbcommand mycommand=new oledbcommand("select * from 版本",myconnect);
oledbdataadapter mydataadapter=new oledbdataadapter();
mydataadapter.selectcommand=mycommand;
oledbcommandbuilder mycommandbuilder=new oledbcommandbuilder(mydataadapter);
myconnect.open();
//获取已有的数据
m_dataset=new dataset();
try
{
mydataadapter.fill(m_dataset,this.m_tablename);
//如果是首次上传,则增加一条记录
if(m_dataset.tables[m_tablename].rows.count==0)
{
datarow newrow=m_dataset.tables[m_tablename].newrow();
newrow["序号"]="1";
m_dataset.tables[m_tablename].rows.add(newrow);
}
datarow row=m_dataset.tables[m_tablename].rows[0];
//填入去掉路径的文件名称
row["文件名称"]=this.getfilenamefrompath(this.txtfilename.text.trim());
//填入版本号
row["版本号"]=this.txtversion.text.trim();
//将实际文件存入记录中
filestream fs=new filestream(this.txtfilename.text.trim(),filemode.open);
byte [] mydata = new byte [fs.length ];
fs.position = 0;
fs.read (mydata,0,convert.toint32 (fs.length ));
row["文件内容"] = mydata;
fs.close();//关闭文件
//更新数据库
mydataadapter.update(this.m_dataset,this.m_tablename);
myconnect.close();
messagebox.show("文件更新成功!");
}
catch(exception ee)
{
messagebox.show(ee.message);
}
}
else
{
messagebox.show("请输入文件名");
}
}
至此,上传工具制作完成,通过该程序,可以上传主程序文件,当然,该工具是给软件开发供应商用于发布新软件用的,千万不要给用户哦。
最后是编写登录程序,按照编写上传工具的方法添加一个项目,项目名称为login,设置输出路径为d:/output,并设置该项目为启动项目。
添加一个组合框(combusername),设置dropdownstyle为dropdownlist,用来选择已有的用户名,添加一个用于输入密码的文本框(txtpassword),设置passwordchar属性为“*”,并在前面加入相应的文字标签,再添加确定(btnok)和取消(btncancel)按钮,并将确定按钮的enable属性设置为false,目的是如果新软件没有下载完成,不准登录,布置如下图:
切换到代码窗口,添加引用:
using system.data.oledb;
using system.threading;
using system.io;
using microsoft.win32;
再添加如下变量:
/// <summary>
/// 存放操作员及密码的dataset
/// </summary>
private dataset m_dataset;
/// <summary>
/// 本功能用到的数据库表
/// </summary>
private string m_tablename="操作员";
private datatable m_table;
为了避免每次都下载主程序,我们将当前主程序的版本号要保存下来,我采用的办法是保存到注册表中,为此,写两个函数,用于读取/写入注册表,如下:
/// <summary>
/// 定义本软件在注册表中software下的公司名和软件名称
/// </summary>
private string m_companyname="lqjt",m_softwarename="autologin";
/// <summary>
/// 从注册表中读信息;
/// </summary>
/// <param name="p_keyname">要读取的键值</param>
/// <returns>读到的键值字符串,如果失败(如注册表尚无信息),则返回""</returns>
private string readinfo(string p_keyname)
{
registrykey softwarekey=registry.localmachine.opensubkey("software",true);
registrykey companykey=softwarekey.opensubkey(m_companyname);
string strvalue="";
if(companykey==null)
return "";
registrykey softwarenamekey=companykey.opensubkey(m_softwarename);//建立
if(softwarenamekey==null)
return "";
try
{
strvalue=softwarenamekey.getvalue(p_keyname).tostring().trim();
}
catch
{}
if(strvalue==null)
strvalue="";
return strvalue;
}
/// <summary>
/// 将信息写入注册表
/// </summary>
/// <param name="p_keyname">键名</param>
/// <param name="p_keyvalue">键值</param>
private void writeinfo(string p_keyname,string p_keyvalue)
{
registrykey softwarekey=registry.localmachine.opensubkey("software",true);
registrykey companykey=softwarekey.createsubkey(m_companyname);
registrykey softwarenamekey=companykey.createsubkey(m_softwarename);
//写入相应信息
softwarenamekey.setvalue(p_keyname,p_keyvalue);
}
再写一个函数,用户来获取用户名/密码和更新主程序版本:
/// <summary>
/// 获取操作员情况,同时更新主程序版本
/// </summary>
private void getinfo()
{
this.m_dataset=new dataset();
this.combusers.items.clear();
string strsql=string.format("select * from 操作员 order by 姓名");
//连接数据库
string strconnection="provider = microsoft.jet.oledb.4.0 ;jet oledb:database password=;data source ="+
application.startuppath.tostring().trim()+"//mydatabase.mdb" ;
oledbconnection myconnect=new oledbconnection(strconnection);
oledbcommand mycommand=new oledbcommand(strsql,myconnect);
oledbdataadapter mydataadapter=new oledbdataadapter();
mydataadapter.selectcommand=mycommand;
try
{
myconnect.open();
//获取操作员信息
mydataadapter.fill(this.m_dataset,this.m_tablename);
//将查询到的用户名填充到组合框供用户选择
this.m_table=this.m_dataset.tables[this.m_tablename];
foreach(datarow row in m_dataset.tables[m_tablename].rows)
{
this.combusers.items.add(row["姓名"]).tostring().trim();
}
//检查是否有新的版本
dataset dataset=new dataset();
string tablename="tablename";
//为减少数据传送时间,不获取文件内容
strsql="select 文件名称,版本号 from 版本";
mycommand=new oledbcommand(strsql,myconnect);
mydataadapter=new oledbdataadapter();
mydataadapter.selectcommand=mycommand;
mydataadapter.fill(dataset,tablename);
if(dataset.tables[tablename].rows.count==1)//有文件
{
string filename=dataset.tables[tablename].rows[0]["文件名称"].tostring();
string version=dataset.tables[tablename].rows[0]["版本号"].tostring();
//读入本机主程序的版本号
string oldversion=this.readinfo(filename);
if(oldversion.length==0)//不存在
oldversion="0";
if(decimal.parse(version)>decimal.parse(oldversion))//有新的版本出现
{
//取回文件内容
dataset=new dataset();
strsql="select * from 版本";
mycommand=new oledbcommand(strsql,myconnect);
mydataadapter=new oledbdataadapter();
mydataadapter.selectcommand=mycommand;
mydataadapter.fill(dataset,tablename);
//将文件下载到本地
datarow row=dataset.tables[tablename].rows[0];
if(row["文件内容"]!=dbnull.value)
{
byte[] byteblobdata = new byte[0];
byteblobdata = (byte[])row["文件内容"];
try
{
filestream fs=new filestream(application.startuppath+"//"+filename,filemode.openorcreate);
fs.write(byteblobdata,0,byteblobdata.length);
fs.close();
//写入当前版本号,供下次使用
this.writeinfo(filename,version);
}
catch(exception ee)
{
messagebox.show(ee.message);
}
}
}//有新版本
}//有文件
//关闭连接
myconnect.close();
}
catch(exception ee)
{
messagebox.show(ee.message);
return;
}
//允许登录
this.btnok.enabled=true;
}
为了不让用户等待太久,在启动时通过一个线程,让获取用户信息和更新在后台完成,即在窗口load事件中,通过线程调用上面的getinfo的函数,故窗口load代码如下:
private void form1_load(object sender, system.eventargs e)
{
//为加快显示速度,将数据库连接等放到另外一个线程中去
thread thread=new thread(new threadstart(getinfo));
thread.start();
}
有了上述准备,我们来编写确定按钮的响应代码如下:
private void btnok_click(object sender, system.eventargs e)
{
//根据组合框的选择,得到当前用户在dataset中具体物理位置
if(this.combusers.selectedindex<0)//没有选择
return;
datarow rownow=null;
foreach(datarow row in this.m_dataset.tables[this.m_tablename].rows)
{
if(row["姓名"].tostring().trim()==this.combusers.text.trim())
{
rownow=row;
break;
}
}
if(rownow==null)
return;
//获取当前正确密码
string strpassword=rownow["密码"].tostring().trim();
this.txtpassword.text=this.txtpassword.text.trim();
if(this.txtpassword.text==strpassword)//密码正确
{
//主程序名称
string filename=application.startuppath+"//"+"mainpro.exe";
//参数名称
string arg=this.combusers.text+" "+this.txtpassword.text;
//运行主程序
system.diagnostics.process fun=system.diagnostics.process.start(filename,arg);
//关闭登录框
this.close();
}
else
{
messagebox.show(" 密码错误!如果你确信密码输入正确,/n可以试着检查一下大写字母键是否按下去了。",
"警告",messageboxbuttons.ok,messageboxicon.warning);
this.txtpassword.focus();
this.txtpassword.selectall();
}
}
取消按钮的代码非常简单,就是关闭登录窗口:
private void btncancel_click(object sender, system.eventargs e)
{
this.close();
}
把login和mainpro软件连同其他相关文件打包成安装程序,将login以快捷方式放到桌面或开始菜单中供用户使用(当然,快捷方式名称可以随便取了),用户运行login后,会自动更新软件。
本例中所有代码请到ftp://qydn.vicp.net/ 下载,文件名为update.rar,解压缩后别忘了在d:/创建一个output文件夹,并将mydatabase.mdb复制到该文件夹中。
说明:本文只起抛砖引玉的作用,通过该思路进行扩展可以完成许多功能,如通过修改上传/登录程序,不仅可以实现对主程序的更新,而且可以实现对任何要用到的资源文件进行更新,本例中不能实现对登录框本身的更新,我采用的办法是在主程序的closing事件中更新登录窗口,因为此时登录窗口已经关闭了。在登录窗口中,可以放一个“保存密码”的复选框,如果用户选中该组合框,可以将用户名和密码保存到注册表中,下次登录时直接读入,用户只要点确定按钮即可,免去了每次都选用户名和输密码的烦恼,
在本例中,我们可以看到,数据库的连接、查询等工作是重复性劳动,且三个个项目中用到的数据库、公司名称等是一样的,在实际工作中,我们可以单独新建一个cs文件,不妨取名为mytools.cs,将一些常用函数和变量(如数据库连接、公司名称等)做成静态的,各具体项目中链接本文件,然后直接使用,我们只需修改mytools.cs中的相关变量或函数而不必在每个项目中都去改,既方便又不会遗漏,mytools.cs参考如下:
///<summary>
///预编译选项,如果定义了networkversion,,表示是网络版,使用sql2000数据库,否则,使用access2000数据库
///</summary>
//#define networkversion
using system;
using system.drawing;
using system.collections;
using system.componentmodel;
using system.windows.forms;
using system.drawing.imaging;
using system.io;
using system.data;
#if networkversion
using system.data.sqlclient;
#else
using system.data.oledb;
#endif
using system.reflection;
using microsoft.win32;
namespace oa
{
public class tool
{
public tool()
{
}
/// <summary>
/// 主程序的文件名
/// </summary>
public const string filename="oa.exe";
public const string g_titlename="丽汽集团办公自动化系统";
public static string g_username;
public static void writeinfo(string p_keyname,string p_keyvalue)
{
……
}
//其他类似代码略……
}
}
如果一个项目中要用到mytools中的内容,可以按如下方式进行:
在“解决方案资源管理器”窗口中选择该项目,选择菜单“项目”→“添加现有项”,此时弹出打开文件对话框,文件类型设为所有文件(*.*),找到mytools.cs,不要直接点打开按钮,看到了打开按钮后面的“↓”了吗?单击它可以弹出一个菜单,选择“链接文件(l)”,这样插入的文件只是一个链接,不会生成副本(如下图)。
使用时,添加mytools的应用,再使用tool类中的公共函数,如:
using oa;
private void myfun()
{
string s=tool.filename;
}
如果单位名称变了,我们只要修改mytools.cs中的变量就可以了,不必到每个项目中都去修改。
我们还注意了一个细节:
///<summary>
///预编译选项,如果定义了networkversion,,表示是网络版,使用sql2000数据库,否则,使用access2000数据库
///</summary>
//#define networkversion
我们知道,对于access或sql server等,除了连接方式外,其余操作几乎完全一样,因此,我们定义了一个选项(如上面的注释),如果#define networkversion,表示是网络版,使用sql server数据库,否则(将#define networkversion注释掉)就是单机版,使用access数据库,在mytools中我们将两种连接方式有区别的地方分别编写,就可以通过是否注释掉#define networkversion这一行分别生成单机版和网络版软件,参考代码如下:
/// <summary>
/// 根据sql语句返回一个查询结果,主要用于只要求返回一个字段的一个结果的情况
/// </summary>
/// <param name="p_sql">查询用到的sql语句</param>
/// <returns>查询到的结果,没有时则返回空""</returns>
public static string getavalue(string p_sql)
{
string strresult="";
tool.openconn();
//设计所需要返回的数据集的内容
try
{
// 打开指向数据库连接
#if networkversion //网络版
sqlcommand acommand = new sqlcommand ( p_sql ,m_connect ) ;
sqldatareader areader = acommand.executereader ( ) ;
#else //单机版,注意变量名acommand和areader在两个版本中都是一样的,有利于编程
oledbcommand acommand = new oledbcommand ( p_sql ,m_connect ) ;
oledbdatareader areader = acommand.executereader ( ) ;
#endif
// 返回需要的数据集内容这里就不分单机版还是网络版了,反正变量名一样
if(areader.read())
strresult=areader[0].tostring();
areader.close () ;
}
catch(exception ee)
{
messagebox.show(ee.message);
}
return strresult;
}
以上类似的小技巧还很多,注意总结,定会收益多多。