在C#中实现软件自动升级

时间:2021-03-30 04:51:17

在C#中实现软件自动升级

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;
      }
以上类似的小技巧还很多,注意总结,定会收益多多。