應用程式生命週期 API 的應用程式實例

應用程式的實例模型會判斷應用程式的多個實例是否可以同時執行。

必要條件

若要在 Windows 應用程式 SDK 中使用應用程式生命週期 API:

  1. 下載並安裝最新版的 Windows 應用程式 SDK。 如需詳細資訊,請參閱安裝 Windows 應用程式 SDK 的工具。
  2. 請依照指示建立您的第一個 WinUI 3 專案,或使用現有專案中的 Windows 應用程式 SDK。

單一實例應用程式

注意

如需如何使用 C# 在 WinUI 3 應用程式中實作單一實例的範例,請參閱 在 Windows 開發人員部落格上建立應用程式單一實例

如果一次只能有一個主要進程執行,應用程式就會是單一實例。 嘗試啟動單一實例應用程式的第二個實例,通常會導致第一個實例的主視窗改為啟動。 請注意,這隻適用於主要進程。 單一實例應用程式可以建立多個背景進程,但仍視為單一實例。

UWP 應用程式預設為單一實例。 但能夠藉由在啟動時間決定建立其他實例或改為啟動現有的實例,來成為多重實例。

Windows Mail 應用程式是單一實例應用程式的良好範例。 當您第一次啟動Mail時,將會建立新的視窗。 如果您再次嘗試啟動 [郵件],則會改為啟動現有的 [郵件] 視窗。

多重實例應用程式

如果主要進程可以同時執行多次,應用程式就會多實例化。 嘗試啟動多實例應用程式的第二個實例會建立新的進程和主視窗。

傳統上,未封裝的應用程式預設為多重實例,但在一定的情況下可以實作單一實例。 這通常是使用單一具名 Mutex 來完成,以指出應用程式是否已執行。

記事本 是多重實例應用程式的良好範例。 每次您嘗試啟動 記事本 時,都會建立新的 記事本 實例,而不論有多少實例已在執行中。

Windows 應用程式 SDK 實例與UWP實例有何不同

Windows 應用程式 SDK 中的實例行為是以UWP的模型、類別為基礎,但有一些主要差異:

AppInstance 類別

實例清單

  • UWPGetInstances 只會傳回應用程式明確註冊以進行潛在重新導向的實例。
  • Windows 應用程式 SDK:GetInstances 會傳回使用AppInstance API 之應用程式的所有執行中實例,無論它們是否已註冊密鑰。 這可以包含目前的實例。 如果您要將目前的實體包含在清單中,請呼叫 AppInstance.GetCurrent。 針對相同應用程式的不同版本,以及不同使用者所啟動的應用程式實例,會維護個別清單。

註冊金鑰

多重實例應用程式的每個實例都可以透過 FindOrRegisterForKey 方法註冊任意密鑰。 索引鍵沒有固有的意義;應用程式可以使用任何形式或想要的方式使用金鑰。

應用程式的實例可以隨時設定其密鑰,但每個實例只允許一個密鑰;設定新值會覆寫先前的值。

應用程式的實例無法將其索引鍵設定為另一個實例已註冊的相同值。 嘗試註冊現有的金鑰會導致 FindOrRegisterForKey 傳回已註冊該金鑰的應用程式實例。

  • UWP:實例必須註冊密鑰,才能包含在從 GetInstances 傳回的清單中。
  • Windows 應用程式 SDK:註冊金鑰與實例清單分離。 實例不需要註冊金鑰,才能包含在清單中。

取消註冊金鑰

應用程式的實例可以取消註冊其密鑰。

  • UWP:當實例取消註冊其密鑰時,它就不再可供啟用重新導向使用,而且不會包含在從 GetInstances 傳回的實例清單中。
  • Windows 應用程式 SDK:尚未註冊其密鑰的實例仍可供啟用重新導向使用,但仍包含在 GetInstances回的實例清單中。

實例重新導向目標

應用程式的多個實例可以互相啟動,稱為「啟用重新導向」的程式。 例如,如果啟動時找不到應用程式的其他實例,應用程式可能會實作單一實例,並改為在另一個實例存在時重新導向和結束。 根據該應用程式的商業規則,多重實例應用程式可以在適當時重新導向啟用。 當啟用重新導向至另一個實例時,它會使用該實例的 Activated 回呼,這是所有其他啟用案例中使用的相同回呼。

  • UWP:只有已註冊密鑰的實例才能成為重新導向的目標。
  • Windows 應用程式 SDK:任何實例都可以是重新導向目標,無論其是否有已註冊的密鑰。

重新導向後行為

  • UWP:重新導向是終端機作業;即使重新導向失敗,應用程式會在重新導向之後終止。

  • Windows 應用程式 SDK:在 Windows 應用程式 SDK 中,重新導向不是終端機作業。 這部分反映了任意終止可能已配置某些記憶體的 Win32 應用程式的潛在問題,但也允許支援更複雜的重新導向案例。 請考慮執行大量CPU密集型工作時,實例會收到啟用要求的多實例應用程式。 該應用程式可以將啟用要求重新導向至另一個實例,並繼續處理。 如果應用程式在重新導向之後終止,就無法達到該案例。

啟用要求可以重新導向多次。 實例 A 可以重新導向至實例 B,而實例 B 可能會接著重新導向至實例 C。Windows 應用程式 SDK 利用這項功能的應用程式必須防範迴圈重新導向 - 如果上述範例中的 C 重新導向至 A,則可能會有無限啟用迴圈。 應用程式取決於應用程式支援哪些工作流程,判斷如何處理迴圈重新導向。

啟用事件

為了處理重新啟用,應用程式可以註冊 Activated 事件。

  • UWP:事件會將 IActivatedEventArgs 傳遞至應用程式。
  • Windows 應用程式 SDK:事件會將 Microsoft.Windows.AppLifecycle.AppActivationArguments 實例傳遞至包含其中-ActivatedEventArgs一個實例的應用程式。

範例

處理啟用

此範例示範應用程式如何註冊及處理 Activated 事件。 當它收到 Activated 事件時,此應用程式會使用事件自變數來判斷造成啟用的動作類型,並適當地回應。

int APIENTRY wWinMain(
    _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // Initialize the Windows App SDK framework package for unpackaged apps.
    HRESULT hr{ MddBootstrapInitialize(majorMinorVersion, versionTag, minVersion) };
    if (FAILED(hr))
    {
        OutputFormattedDebugString(
            L"Error 0x%X in MddBootstrapInitialize(0x%08X, %s, %hu.%hu.%hu.%hu)\n",
            hr, majorMinorVersion, versionTag, 
            minVersion.Major, minVersion.Minor, minVersion.Build, minVersion.Revision);
        return hr;
    }

    if (DecideRedirection())
    {
        return 1;
    }

    // Connect the Activated event, to allow for this instance of the app
    // getting reactivated as a result of multi-instance redirection.
    AppInstance thisInstance = AppInstance::GetCurrent();
    auto activationToken = thisInstance.Activated(
        auto_revoke, [&thisInstance](
            const auto& sender, const AppActivationArguments& args)
        { OnActivated(sender, args); }
    );

    // Carry on with regular Windows initialization.
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_CLASSNAME, szWindowClass, MAX_LOADSTRING);
    RegisterWindowClass(hInstance);
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    MddBootstrapShutdown();
    return (int)msg.wParam;
}

void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
    int const arraysize = 4096;
    WCHAR szTmp[arraysize];
    size_t cbTmp = arraysize * sizeof(WCHAR);
    StringCbPrintf(szTmp, cbTmp, L"OnActivated (%d)", activationCount++);

    ExtendedActivationKind kind = args.Kind();
    if (kind == ExtendedActivationKind::Launch)
    {
        ReportLaunchArgs(szTmp, args);
    }
    else if (kind == ExtendedActivationKind::File)
    {
        ReportFileArgs(szTmp, args);
    }
}

以啟用類型為基礎的重新導向邏輯

在此範例中,應用程式會註冊 Activated 事件的處理程式,也會檢查啟用事件自變數,以決定是否要將啟用重新導向至另一個實例。

針對大部分的啟用類型,應用程式會繼續執行其一般初始化程式。 不過,如果啟用是由開啟關聯的檔類型所造成,而且如果此應用程式的另一個實例已經開啟檔案,則目前的實例會將啟用重新導向至現有的實例並結束。

此應用程式會使用金鑰註冊來判斷哪些檔案會在哪個實例中開啟。 當實例開啟檔案時,它會註冊包含該檔名的密鑰。 然後,其他實例可以檢查已註冊的密鑰,並尋找特定的檔名,並在沒有其他實例時自行註冊為該檔案的實例。

請注意,雖然金鑰註冊本身是 Windows 應用程式 SDK 應用程式生命週期 API 的一部分,但密鑰的內容只會在應用程式本身內指定。 應用程式不需要註冊檔名或任何其他有意義的數據。 不過,此應用程式已決定根據其特定需求和支援的工作流程,透過金鑰追蹤開啟的檔案。

bool DecideRedirection()
{
    // Get the current executable filesystem path, so we can
    // use it later in registering for activation kinds.
    GetModuleFileName(NULL, szExePath, MAX_PATH);
    wcscpy_s(szExePathAndIconIndex, szExePath);
    wcscat_s(szExePathAndIconIndex, L",1");

    // Find out what kind of activation this is.
    AppActivationArguments args = AppInstance::GetCurrent().GetActivatedEventArgs();
    ExtendedActivationKind kind = args.Kind();
    if (kind == ExtendedActivationKind::Launch)
    {
        ReportLaunchArgs(L"WinMain", args);
    }
    else if (kind == ExtendedActivationKind::File)
    {
        ReportFileArgs(L"WinMain", args);

        try
        {
            // This is a file activation: here we'll get the file information,
            // and register the file name as our instance key.
            IFileActivatedEventArgs fileArgs = args.Data().as<IFileActivatedEventArgs>();
            if (fileArgs != NULL)
            {
                IStorageItem file = fileArgs.Files().GetAt(0);
                AppInstance keyInstance = AppInstance::FindOrRegisterForKey(file.Name());
                OutputFormattedMessage(
                    L"Registered key = %ls", keyInstance.Key().c_str());

                // If we successfully registered the file name, we must be the
                // only instance running that was activated for this file.
                if (keyInstance.IsCurrent())
                {
                    // Report successful file name key registration.
                    OutputFormattedMessage(
                        L"IsCurrent=true; registered this instance for %ls",
                        file.Name().c_str());
                }
                else
                {
                    keyInstance.RedirectActivationToAsync(args).get();
                    return true;
                }
            }
        }
        catch (...)
        {
            OutputErrorString(L"Error getting instance information");
        }
    }
    return false;
}

任意重新導向

此範例會藉由新增更複雜的重新導向規則,來擴充上一個範例。 應用程式仍會執行上一個範例的開啟檔案檢查。 不過,如果先前的範例未根據開啟的檔案檢查重新導向,則一律會建立新的實例,本範例會新增「可重複使用」實例的概念。 如果找到可重複使用的實例,則目前的實例會重新導向至可重複使用的實例並結束。 否則,它會將其本身註冊為可重複使用,並繼續其一般初始化。

同樣地,請注意,應用程式生命週期 API 中不存在「可重複使用」實例的概念;它只會在應用程式本身內建立及使用。

int APIENTRY wWinMain(
    _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    // Initialize COM.
    winrt::init_apartment();

    AppActivationArguments activationArgs =
        AppInstance::GetCurrent().GetActivatedEventArgs();

    // Check for any specific activation kind we care about.
    ExtendedActivationKind kind = activationArgs.Kind;
    if (kind == ExtendedActivationKind::File)
    {
        // etc... as in previous scenario.
    }
    else
    {
        // For other activation kinds, we'll trawl all instances to see if
        // any are suitable for redirecting this request. First, get a list
        // of all running instances of this app.
        auto instances = AppInstance::GetInstances();

        // In the simple case, we'll redirect to any other instance.
        AppInstance instance = instances.GetAt(0);

        // If the app re-registers re-usable instances, we can filter for these instead.
        // In this example, the app uses the string "REUSABLE" to indicate to itself
        // that it can redirect to a particular instance.
        bool isFound = false;
        for (AppInstance instance : instances)
        {
            if (instance.Key == L"REUSABLE")
            {
                isFound = true;
                instance.RedirectActivationToAsync(activationArgs).get();
                break;
            }
        }
        if (!isFound)
        {
            // We'll register this as a reusable instance, and then
            // go ahead and do normal initialization.
            winrt::hstring szKey = L"REUSABLE";
            AppInstance::FindOrRegisterForKey(szKey);
            RegisterClassAndStartMessagePump(hInstance, nCmdShow);
        }
    }
    return 1;
}

重新導向協調流程

此範例會再次新增更複雜的重新導向行為。 在這裡,應用程式實例可以自行註冊為處理特定類型所有啟用的實例。 當應用程式的實例收到 Protocol 啟用時,它會先檢查已註冊以處理 Protocol 啟用的實例。 如果找到該實例,則會將啟用重新導向至該實例。 如果沒有,則目前的實例會自行註冊啟用 Protocol ,然後套用其他邏輯(未顯示),這可能會因為其他原因而重新導向啟用。

void OnActivated(const IInspectable&, const AppActivationArguments& args)
{
    const ExtendedActivationKind kind = args.Kind;

    // For example, we might want to redirect protocol activations.
    if (kind == ExtendedActivationKind::Protocol)
    {
        auto protocolArgs = args.Data().as<ProtocolActivatedEventArgs>();
        Uri uri = protocolArgs.Uri();

        // We'll try to find the instance that handles protocol activations.
        // If there isn't one, then this instance will take over that duty.
        auto instance = AppInstance::FindOrRegisterForKey(uri.AbsoluteUri());
        if (!instance.IsCurrent)
        {
            instance.RedirectActivationToAsync(args).get();
        }
        else
        {
            DoSomethingWithProtocolArgs(uri);
        }
    }
    else
    {
        // In this example, this instance of the app handles all other
        // activation kinds.
        DoSomethingWithNewActivationArgs(args);
    }
}

不同於的 UWP 版本RedirectActivationTo,Windows 應用程式 SDK 的 RedirectActivationToAsync作需要在重新導向啟用時明確傳遞事件自變數。 這是必要的,因為 UWP 會嚴格控制啟用,而且可以確保正確的啟用自變數會傳遞至正確的實例,Windows 應用程式 SDK 的版本支援許多平臺,而且不能依賴 UWP 特定功能。 此模型的其中一個優點是,使用 Windows 應用程式 SDK 的應用程式有機會修改或取代將傳遞至目標實例的自變數。

重新導向而不封鎖

在進行不必要的初始化工作之前,大部分的應用程式都希望儘早重新導向。 針對某些應用程式類型,初始化邏輯會在 STA 線程上執行,而該線程不得遭到封鎖。 AppInstance.RedirectActivationToAsync 方法是異步的,而且呼叫的應用程式必須等候方法完成,否則重新導向將會失敗。 不過,等候異步呼叫將會封鎖 STA。 在這些情況下,在另一個線程中呼叫 RedirectActivationToAsync,並在呼叫完成時設定事件。 然後使用 CoWaitForMultipleObjects 等非封鎖 API 等候該事件。 以下是 WPF 應用程式的 C# 範例。

private static bool DecideRedirection()
{
    bool isRedirect = false;

    // Find out what kind of activation this is.
    AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
    ExtendedActivationKind kind = args.Kind;
    if (kind == ExtendedActivationKind.File)
    {
        try
        {
            // This is a file activation: here we'll get the file information,
            // and register the file name as our instance key.
            if (args.Data is IFileActivatedEventArgs fileArgs)
            {
                IStorageItem file = fileArgs.Files[0];
                AppInstance keyInstance = AppInstance.FindOrRegisterForKey(file.Name);

                // If we successfully registered the file name, we must be the
                // only instance running that was activated for this file.
                if (keyInstance.IsCurrent)
                {
                    // Hook up the Activated event, to allow for this instance of the app
                    // getting reactivated as a result of multi-instance redirection.
                    keyInstance.Activated += OnActivated;
                }
                else
                {
                    isRedirect = true;

                    // Ensure we don't block the STA, by doing the redirect operation
                    // in another thread, and using an event to signal when it has completed.
                    redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
                    if (redirectEventHandle != IntPtr.Zero)
                    {
                        Task.Run(() =>
                        {
                            keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
                            SetEvent(redirectEventHandle);
                        });
                        uint CWMO_DEFAULT = 0;
                        uint INFINITE = 0xFFFFFFFF;
                        _ = CoWaitForMultipleObjects(
                            CWMO_DEFAULT, INFINITE, 1, 
                            new IntPtr[] { redirectEventHandle }, out uint handleIndex);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error getting instance information: {ex.Message}");
        }
    }

    return isRedirect;
}

取消註冊重新導向

註冊金鑰的應用程式可以隨時取消註冊該金鑰。 此範例假設目前實例先前已註冊索引鍵,指出它已開啟特定檔案,這表示後續嘗試開啟該檔案將會重新導向至該檔案。 關閉該檔案時,必須刪除包含檔名的索引鍵。

void CALLBACK OnFileClosed(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    AppInstance::GetCurrent().UnregisterKey();
}

警告

雖然索引鍵會在進程終止時自動取消註冊,但在取消註冊終止的實例之前,可能會有另一個實例起始重新導向至終止的實例。 為了減輕這種可能性,應用程式可以使用 UnregisterKey 在終止密鑰之前手動取消註冊金鑰,讓應用程式有機會將啟用重新導向至不在結束過程中的另一個應用程式。

實例資訊

Microsoft.Windows.AppLifeycle.AppInstance 類別代表應用程式的單一實例。 在目前的預覽中, AppInstance 只包含支援啟用重新導向所需的方法和屬性。 在更新版本中, AppInstance 將會展開以包含與應用程式實例相關的其他方法和屬性。

void DumpExistingInstances()
{
    for (AppInstance const& instance : AppInstance::GetInstances())
    {
        std::wostringstream sStream;
        sStream << L"Instance: ProcessId = " << instance.ProcessId
            << L", Key = " << instance.Key().c_str() << std::endl;
        ::OutputDebugString(sStream.str().c_str());
    }
}