防止 WinForm 重複啟動並將工作轉交已存在程序
1 |
這幾天在寫 WinForm 程式,遇到有意思的需求。小程式透過 Registry Shell/Open/Command 註冊方式啟動(類似在 Chrome/Edge 網頁用 IE 開啟超連結所介紹的技巧),以 MyApp.exe %1 方式接收參數執行任務。
這裡用個簡化範例模擬我遇到的挑戰。
Program.cs Main() 從 string[] args 取得外部傳入字串,稍後交給 WinForm 處理。 除了呼叫時傳入參數,程式還有其他方式取得待處理字串,故我用 Queue<string> 儲存待處理字串。 不管啟動時傳入或後續由其他管道接收,一律放入 Queue 中,WinForm 再以 Timer 定期檢查統一由 Queue 取出字串進行處理:
static class Program
{
public static Queue<string> Messages = new Queue<string>();
[STAThread]
static void Main(string[] args)
{
if (args.Any())
{
Messages.Enqueue(args[0]);
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
Form1 只簡單放了 ListBox lbMessages 跟 Timer Timer1,Timer1 每 0.1 秒檢查一次 Queue, 只要其中有字串,就取出加入 ListBox 顯示出來:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Timer1_Tick(object sender, EventArgs e)
{
if (Program.Messages.Any())
{
lbMessages.Items.Add(Program.Messages.Dequeue());
}
}
}
簡單寫完,實測也 Work。但有個困擾,就是重複呼叫時會跑出多個視窗:
使用者每點一下網頁連結就跑一個新視窗,感覺不太優。 幸好,之前學過用 Mutex 限定程式只執行一份的技巧(延伸閱讀:防止程式同時執行多份,比檢查 Process 清單更好的方法), 要限定不准重複啟動難不倒我。Program.cs 修改如下:
static class Program
{
public static Queue<string> Messages = new Queue<string>();
static string appGuid = "{8604F243-A7AE-45C9-AB4C-D129FEA9FEE9}";
[STAThread]
static void Main(string[] args)
{
using (Mutex m = new Mutex(false, "Global\\" + appGuid))
{
//另一份已執行
if (!m.WaitOne(0, false))
{
MessageBox.Show("程式已在執行中");
return;
}
else
{
if (args.Any())
{
Messages.Enqueue(args[0]);
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
}
很好,這樣就能阻止多個視窗產生。
但問題只解決了一半,第二次呼叫時傳入的參數應該要移交已在執行中的程序處理,而不是隨著後啟動程序關閉消失無蹤。 因此,後起程序偵測到已有相同程式在執行,關閉前應設法通知執行中的程序,將工作移交過去,這種跨程序溝通是標準的 Interprocess Communictaion(IPC) 情境。
關於 Shared Memory 的兩三事一文列舉過 IPC 有多種選擇,如 Data Copy、DDE、File Mapping、Pipes、RPC、Socket 等。 由於本案例屬低頻率微流量的資訊交換,不想殺雞用牛刀,我希望做法愈省事愈好。評估後,決定試試之前沒玩過且特別適合 WinForm 前景程式的 Data Copy (WM_COPYDATA)。
經過好一陣摸索,最後試出來最簡潔的可行寫法是引用一個封裝好的 SendMessage 字串傳遞共用函式(來源:C# Win32 messaging with SendMessage and WM_COPYDATA),配合我的應用改寫並簡化:
public class MessageHelper
{
[DllImport("User32.dll", EntryPoint = "FindWindow")]
public static extern Int32 FindWindow(String lpClassName, String lpWindowName);
[DllImport("User32.dll", EntryPoint = "SendMessage")]
public static extern int SendMessage(int hWnd, int Msg, int wParam, ref COPYDATASTRUCT lParam);
[DllImport("User32.dll", EntryPoint = "SetForegroundWindow")]
public static extern bool SetForegroundWindow(int hWnd);
public const int WM_COPYDATA = 0x4A;
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
[MarshalAs(UnmanagedType.LPStr)]
public string lpData;
}
public bool BringAppToFront(int hWnd)
{
return SetForegroundWindow(hWnd);
}
public int SendWindowsStringMessage(int hWnd, int wParam, string msg)
{
int result = 0;
if (hWnd > 0)
{
byte[] sarr = System.Text.Encoding.Default.GetBytes(msg);
int len = sarr.Length;
COPYDATASTRUCT cds;
cds.dwData = (IntPtr)100;
cds.lpData = msg;
cds.cbData = len + 1;
result = SendMessage(hWnd, WM_COPYDATA, wParam, ref cds);
}
return result;
}
public int GetWindowId(string className, string windowName)
{
return FindWindow(className, windowName);
}
public static void WndProc(ref System.Windows.Forms.Message m,
Action<string> callback)
{
if (m.Msg == WM_COPYDATA)
{
var cds = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT));
callback(cds.lpData);
}
}
}
工具就緒,來看看程式要如何整合應用 SendMessage 達成溝通。
Program.cs 發現另有一份執行時,要利用 Form1 的名稱(Form1.Text,定義成常數確保 Program 與 Form1 一致)找到 Form1 的 hWnd。 再透過 SendWindowsStringMessage 將 args[0] 傳送給 Form1:
public const string FormName = "WMCopyDataForm\t";
[STAThread]
static void Main(string[] args)
{
//REF: https://blog.darkthread.net/blog/9952/
using (Mutex m = new Mutex(false, "Global\\" + appGuid))
{
//另一份已執行
if (!m.WaitOne(0, false))
{
if (args.Any())
{
//透過SendMessage將訊息傳給執行中的視窗
var mh = new MessageHelper();
var hwnd = mh.GetWindowId(null, FormName);
mh.BringAppToFront(hwnd);
var res = mh.SendWindowsStringMessage(hwnd, 0, args[0]);
}
return;
}
else
{
if (args.Any())
{
Messages.Enqueue(args[0]);
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
Form1.cs 則加上 WinProc(ref Message m) 方法處理外部傳入資訊, 當接收到 WM_COPYDATA 時,將其還原成原本的資料結構取出字串,將其填入 Queue。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.Text = Program.FormName;
}
protected override void WndProc(ref Message m)
{
MessageHelper.WndProc(ref m, s =>
{
Program.Messages.Enqueue(s);
});
base.WndProc(ref m);
}
private void Timer1_Tick(object sender, EventArgs e)
{
if (Program.Messages.Any())
{
lbMessages.Items.Add(Program.Messages.Dequeue());
}
}
}
實測一下。第二次呼叫不會產生新視窗,傳入字串顯示在已存在的視窗,成功! (灑花)
A example showing how to use Mutex to prevent duplicated process instances and pass the arguemt to the existing process with WM_COPYDATA.
Comments
# by Berlin
謝謝大大,簡單易懂,給讚!