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("http://");
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()+"http://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()+"http://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+"http://"+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+"http://"+"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;
}
以上類似的小技巧還很多,注意總結,定會收益多多。