實作單一執行個體應用:第二個實例啟動時喚醒第一個實例主視窗並置頂。使用 SetWindowPos(hWnd, HWND_TOPMOST...) 強制置頂後,發現主視窗**無法最小化** (最小化按鈕失效、WM_SYSCOMMAND / SC_MINIMIZE 無反應),但最大化、關閉、操作正常。
VCL 建立隱藏視窗 Application.Handle (類別 TApplication) 作為訊息分派中心。主視窗的 WM_SYSCOMMAND 會轉發給 Application 處理。
procedure TApplication.Minimize;
begin
if not IsIconic(FHandle) then // FHandle = Application.Handle
begin
NormalizeTopMosts;
SetActiveWindow(FHandle);
if (MainForm <> nil) and (ShowMainForm or MainForm.Visible) and IsWindowEnabled(MainForm.Handle) then
begin
SetWindowPos(FHandle, MainForm.Handle, MainForm.Left, MainForm.Top, MainForm.Width, 0, SWP_SHOWWINDOW);
DefWindowProc(FHandle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
end
else
ShowWinNoAnimate(FHandle, SW_MINIMIZE);
if Assigned(FOnMinimize) then FOnMinimize(Self);
end;
end;
SetWindowPos(hMain, HWND_TOPMOST...) 強制置頂WM_SYSCOMMAND/SC_MINIMIZE → 轉發 Application.MinimizeIsIconic(Application.Handle) 回傳 True (系統誤判 Application 已最小化)Minimize 提前返回,**主視窗未收到最小化指令**procedure BringToFrontAndRestore(const hMainWnd: HWND);
begin
// 1. 先處理 Application 隱藏視窗
if IsIconic(Application.Handle) then
begin
// 發送 SC_RESTORE 讓 Application 狀態同步
DefWindowProc(Application.Handle, WM_SYSCOMMAND, SC_RESTORE, 0);
end;
// 2. 再置頂主視窗
SetWindowPos(hMainWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE);
SetWindowPos(hMainWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE);
// 3. 確保主視窗啟用
SetForegroundWindow(hMainWnd); // 或 SetActiveWindow / BringWindowToTop
end;
procedure ForceMinimizeMainForm;
begin
if Application.MainForm <> nil then
PostMessage(Application.MainForm.Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
end;
缺點:繞過 VCL 邏輯,可能影響 OnMinimize 事件觸發、工作列動畫等。
procedure SafeBringToFront(const hWnd: HWND);
begin
// 標準置頂序列
if IsIconic(hWnd) then
ShowWindow(hWnd, SW_RESTORE);
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE or SWP_SHOWWINDOW);
SetForegroundWindow(hWnd);
SetActiveWindow(hWnd);
end;
避免 HWND_TOPMOST 來回切換帶來的 Z-order 混亂。
program SingleInstanceApp;
uses
Vcl.Forms, Winapi.Windows, Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
function IsAlreadyRunning: Boolean;
var
Mutex: THandle;
begin
Mutex := CreateMutex(nil, True, 'Global\MyApp_Unique_Mutex_Name_{GUID}');
Result := (Mutex = 0) or (GetLastError = ERROR_ALREADY_EXISTS);
if Result and (Mutex <> 0) then CloseHandle(Mutex);
end;
procedure WakeUpPreviousInstance;
var
hWnd: HWND;
begin
hWnd := FindWindow('TForm1', nil); // 或用 RegisterWindowMessage 廣播
if hWnd <> 0 then
begin
// 發送自訂訊息喚醒
PostMessage(hWnd, WM_APP + 1, 0, 0);
end;
end;
begin
if IsAlreadyRunning then
begin
WakeUpPreviousInstance;
Exit;
end;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
// Unit1.pas - 主視窗接收喚醒訊息
type
TForm1 = class(TForm)
private
procedure AppWakeUp(var Msg: TMessage); message WM_APP + 1;
end;
procedure TForm1.AppWakeUp(var Msg: TMessage);
begin
// 正確置頂並還原
if IsIconic(Application.Handle) then
DefWindowProc(Application.Handle, WM_SYSCOMMAND, SC_RESTORE, 0);
if WindowState = wsMinimized then
WindowState := wsNormal;
SetWindowPos(Handle, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE);
SetForegroundWindow(Handle);
BringToFront;
end;
{ 簡化流程 }
主視窗 WndProc
→ TControl.WndProc
→ TWinControl.WndProc
→ 若 WM_SYSCOMMAND 且 (SC_MINIMIZE/SC_MAXIMIZE/SC_RESTORE)
→ 發送 CM_SYSCOMMAND 給 Application
→ Application.WndProc
→ 處理 SC_MINIMIZE → TApplication.Minimize
→ 操作 Application.Handle (隱藏視窗)
→ 最後才影響主視窗
關鍵洞見:**Application.Handle 才是系統眼中的「主視窗」**,主視窗只是子視窗。所有系統級視窗狀態 (最小化、最大化、圖示化) 實際作用於 Application.Handle。
HWND_TOP + SetForegroundWindowWindowState 屬性或 ShowWindow API,勿直接發送 WM_SYSCOMMANDApplication.Handle 與主視窗的訊息流向Delphi 的 TApplication 隱藏視窗設計雖優雅統一了訊息處理,但也引入了「雙主視窗」的認知偏差。理解 IsIconic(Application.Handle) 才是真正的視窗狀態判斷,並在跨進程喚醒、置頂、狀態變更時主動同步雙方狀態,即可避免最小化失效等詭異問題。