與 Web 應用程式不同,WinForms 部署在客戶端,更新機制直接影響維運成本。本文提出「啟動器 + 主程式 + 上傳工具」三專案架構,透過資料庫版本比對、背景下載、參數傳遞登入資訊,實現零人工干預的自動更新流程。完整範例包含 Access/SQL Server 双資料庫支援、註冊表版本記錄、多執行緒非阻塞 UI 等實戰技巧。
| 角色 | 專案名稱 | 職責 | 部署對象 |
|---|---|---|---|
| 啟動器/登入器 | Login.exe | 版本檢查、下載更新、驗證帳密、啟動主程式 | 所有用戶(桌面捷徑) |
| 主程式 | MainPro.exe | 核心業務邏輯,僅接受參數啟動 | 用戶(由 Login 啟動) |
| 上傳工具 | UpLoad.exe | 開發端:將新版本主程式上傳至資料庫 | 開發/維運人員 |
-- 操作員表
CREATE TABLE Operators (
SeqNo AUTOINCREMENT PRIMARY KEY,
Name TEXT(50) NOT NULL,
Password TEXT(50) NOT NULL
);
-- 版本表
CREATE TABLE Versions (
SeqNo AUTOINCREMENT PRIMARY KEY,
VersionNo LONG NOT NULL, -- 版本號(整數遞增)
FileName TEXT(100) NOT NULL, -- MainPro.exe
FileContent OLEOBJECT (或 IMAGE) -- 執行檔二進位內容
);
static class Program
{
public static string UserName { get; private set; }
public static string Password { get; private set; }
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length == 2)
{
UserName = args[0];
Password = args[1];
Application.Run(new MainForm());
}
else
{
MessageBox.Show("禁止直接執行,請透過登入器啟動。", "安全性攔截",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
Program.UserName 存取private void btnOK_Click(object sender, EventArgs e)
{
// 1. 驗證版本號格式
if (!decimal.TryParse(txtVersion.Text, out _))
{ MessageBox.Show("版本號必須為數字"); return; }
// 2. 檢查檔案存在
if (!File.Exists(txtFileName.Text))
{ MessageBox.Show("檔案不存在"); return; }
string connStr = GetConnectionString();
using (var conn = new OleDbConnection(connStr))
using (var cmd = new OleDbCommand("SELECT * FROM Versions", conn))
using (var adapter = new OleDbDataAdapter(cmd))
using (var builder = new OleDbCommandBuilder(adapter))
{
conn.Open();
var ds = new DataSet();
adapter.Fill(ds, "Versions");
// 3. 取得或建立版本記錄
DataRow row;
if (ds.Tables["Versions"].Rows.Count == 0)
{
row = ds.Tables["Versions"].NewRow();
row["SeqNo"] = 1;
ds.Tables["Versions"].Rows.Add(row);
}
else row = ds.Tables["Versions"].Rows[0];
// 4. 更新欄位
row["FileName"] = Path.GetFileName(txtFileName.Text);
row["VersionNo"] = txtVersion.Text;
// 5. 讀取二進位內容
byte[] fileData = File.ReadAllBytes(txtFileName.Text);
row["FileContent"] = fileData;
// 6. 寫回資料庫
adapter.Update(ds, "Versions");
MessageBox.Show("上傳成功!");
}
}
public partial class LoginForm : Form
{
private DataSet _operatorDs;
private Thread _initThread;
private void LoginForm_Load(object sender, EventArgs e)
{
btnOK.Enabled = false; // 更新完成前禁用登入
_initThread = new Thread(InitializeAsync);
_initThread.Start();
}
private void InitializeAsync()
{
try
{
LoadOperators(); // 載入帳號下拉選單
CheckAndUpdateApp(); // 檢查並下載更新
}
catch (Exception ex)
{
Invoke(() => MessageBox.Show($"初始化失敗:{ex.Message}"));
}
finally
{
Invoke(() => btnOK.Enabled = true); // 允許登入
}
}
private void LoadOperators()
{
// ... 從資料庫讀取操作員填入 combUserName ...
}
private void CheckAndUpdateApp()
{
string connStr = GetConnectionString();
using (var conn = new OleDbConnection(connStr))
{
conn.Open();
// 只查版本與檔名,不載入二進位內容
using (var cmd = new OleDbCommand("SELECT FileName, VersionNo FROM Versions", conn))
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
string fileName = reader["FileName"].ToString();
int serverVersion = Convert.ToInt32(reader["VersionNo"]);
int localVersion = GetLocalVersion(fileName); // 從註冊表讀取
if (serverVersion > localVersion)
{
DownloadAndReplace(fileName, serverVersion, conn);
}
}
}
}
}
private void DownloadAndReplace(string fileName, int newVersion, OleDbConnection conn)
{
// 重新查詢完整記錄含二進位資料
using (var cmd = new OleDbCommand("SELECT FileContent FROM Versions WHERE FileName=?", conn))
{
cmd.Parameters.AddWithValue("?", fileName);
var content = cmd.ExecuteScalar() as byte[];
if (content != null)
{
string targetPath = Path.Combine(Application.StartupPath, fileName);
File.WriteAllBytes(targetPath, content);
SetLocalVersion(fileName, newVersion); // 寫入註冊表
}
}
}
// 註冊表版本記錄
private int GetLocalVersion(string fileName)
{
using (var key = Registry.LocalMachine.CreateSubKey($@"Software\MyCompany\AutoUpdate"))
{
return int.TryParse(key?.GetValue(fileName)?.ToString(), out int v) ? v : 0;
}
}
private void SetLocalVersion(string fileName, int version)
{
using (var key = Registry.LocalMachine.CreateSubKey($@"Software\MyCompany\AutoUpdate", true))
{
key.SetValue(fileName, version);
}
}
}
private void btnOK_Click(object sender, EventArgs e)
{
if (combUserName.SelectedIndex < 0) return;
// 查找對應帳號
DataRow userRow = _operatorDs.Tables["Operators"].AsEnumerable()
.FirstOrDefault(r => r["Name"].ToString() == combUserName.Text);
if (userRow == null) return;
string correctPwd = userRow["Password"].ToString();
if (txtPassword.Text == correctPwd)
{
string mainExe = Path.Combine(Application.StartupPath, "MainPro.exe");
string args = $"{combUserName.Text} {txtPassword.Text}";
Process.Start(mainExe, args);
this.Close();
}
else
{
MessageBox.Show("密碼錯誤");
txtPassword.Focus();
txtPassword.SelectAll();
}
}
namespace CommonTools
{
public static class AppConfig
{
public const string MainExeName = "MainPro.exe";
public const string RegistryPath = @"Software\MyCompany\AutoUpdate";
#if NETWORK_VERSION
public static string ConnStr => "Server=...;Database=...;User=...;Password=...";
#else
public static string ConnStr => $"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={Application.StartupPath}\\db.mdb";
#endif
}
public static class RegistryHelper
{
public static string Read(string keyName) { /* ... */ }
public static void Write(string keyName, string value) { /* ... */ }
}
}
三個專案以「連結檔案」方式共用 AppConfig.cs,修改一處處處生效。
#define NETWORK_VERSION 切換 Access/SQL Server#if 區隔,變數名保持一致由於執行中的程式無法覆寫自己,採用「主程式關閉時更新登入器」策略:
// MainForm.cs
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
// 檢查登入器版本並下載覆寫 Login.exe
Updater.CheckAndUpdateLauncher();
}
WebClient.DownloadProgressChanged 或 HttpClient)D:\Output),三專案均設定 Output Pathdb.mdb 複製至輸出目錄UpLoad.exe 上傳初始版本 MainPro.exe (版本 1)Login.exe、db.mdb、相關 DLLLogin.exe 捷徑作為用戶進入點本方案以「登入器掌控更新、主程式專注業務、上傳工具隔離部署」為核心,解決了 WinForms 自動更新的經典難題。關鍵技術點包括:資料庫二進位儲存、註冊表版本追蹤、多執行緒非阻塞 UI、參數化安全啟動、共用設定管理。團隊可根據專案規模,將資料庫直連升級為 Web API、引入差分更新、加入數位簽章驗證等進階功能,構建企業級桌面應用交付管線。