1. 程式人生 > >C#實現軟體自動升級

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;

  }

以上類似的小技巧還很多,注意總結,定會收益多多。