WPF 使用 Windows 10 APIs - 1

//build 2018 看到微軟對於 Win32 程式 (WinForms/WPF) 增加新的 SDKs,讓我想起之前做 Desktop Bridge 的心得。

利用這篇介紹幾個例子,幫助大家熟悉怎麽在 WPF 使用 Windows 10 APIs。

Desktop Bridge 有三個大方向

  1. 把既有的 Win32 installer 利用 Desktop App Convert tool 轉成 AppX Package;
  2. 讓既有的 Win32 application 支援 UWP APIs,例如:toast notification, update live tiles 等;
  3. 讓既有的 Win32 application 使用 UWP components,例如:background task;

[重要]

根據 Enhance your desktop application for Windows 10 介紹,在 WPF 專案中加入以下的參考才能使用 Windows 10 APIs:

這些加入的參考,要記得把 Local Copy = false

[注意]

如果遇到 Windows 相關 reference 找不到,直接加入 C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmdC:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\*WindowsRuntime*.dll; 或者利用從 UwpDesktop 加入 Nuget。

如果是 C++ 專案可參考 Modify a Windows Desktop application project to add C++/WinRT support 的設定。

可以參考 UWP APIs available to a packaged desktop app 知道有那些 APIs 可以在 Desktop 下使用,也需注意 API contract 的限制,不同的 contract 會相依于加入參考使用的 Windows 10 版本。

如果遇到一些 API 使用時沒有反應,代表它只能用在擁有 Package identity information,此時就需要 WPF 包裝成 Packages。

封裝方式

  1. 建立 Windows Application Packaging Project;
  2. 在 Packaging Project 加入要包裝的 WPF 專案;

加入參考之後,舉幾個列子説明如何使用:

  • 抓取坐標資訊
    var locator = new Windows.Devices.Geolocation.Geolocator();
    var location = await locator.GetGeopositionAsync();
    var position = location.Coordinate.Point.Position;
    var latlong = string.Format("lat:{0}, long:{1}", position.Latitude, position.Longitude);
    var result = MessageBox.Show(latlong);
    可以參考 How to access the Windows 10 APIs from WPF
  • 發送 Toast
    private async void OnSendToastClick(object sender, RoutedEventArgs e)
    {
        string title = "featured picture of the day";
        string content = "beautiful scenery";
        string image = "https://picsum.photos/360/180?image=104";
        string logo = "https://picsum.photos/64?image=883";
    
        string xmlString =  $@"<toast><visual>
                <binding template='ToastGeneric'>
                <text>{title}</text>
                <text>{content}</text>
                <image src='{image}'/>
                <image src='{logo}' placement='appLogoOverride' hint-crop='circle'/>
                </binding>
                </visual></toast>";
    
        XmlDocument toastXml = new XmlDocument();
        toastXml.LoadXml(xmlString);
        ToastNotification toast = new ToastNotification(toastXml);
        ToastNotificationManager.CreateToastNotifier().Show(toast);
    }
    由於寄送 Toast/Tile 需要 identify,所以要記得建立 Windows Application Packaging Project 搭配使用。 如何處理用戶點擊的 Toast 呢?參考 Send a local toast notification from desktop C# apps 來説明。
  • 建立 Tile
    根據 Create Secondary Tiles from your Desktop Application 介紹,需要利用 COM 的介面操作 Tile 的建立。如下:
    // This interface definition is necessary because this is a non-universal
    // app and we have transfer the hwnd for the window to the WinRT object.
    [ComImport]
    [Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IInitializeWithWindow
    {
        void Initialize(IntPtr hwnd);
    }
    
    private async Task PinToStart()
    {
        // Initialize the tile with required arguments
        SecondaryTile tile = new SecondaryTile("myTileId5391", "Display name", "myActivationArgs", new Uri("ms-appx:///Images/Square150x150Logo.png"), TileSize.Default);
    
        // Assign the window handle
        IInitializeWithWindow initWindow = (IInitializeWithWindow)(object)tile;
        initWindow.Initialize(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
    
        // Pin the tile
        bool isPinned = await tile.RequestCreateAsync();
    }
    
  • 操作 SMTC
    要操作 SMTC 需要改用 MediaPlayer,並設定 MediaPlayer.CommandManager 來注冊按鈕事件與狀態。
    // 建立 MediaPlayer 並注冊 MediaCommonManager 事件
    Player = new MediaPlayer();
    Player.CommandManager.IsEnabled = true;
    Player.CommandManager.PauseReceived += CommandManager_PauseReceived;
    Player.CommandManager.PlayReceived += CommandManager_PlayReceived;
    Player.CommandManager.NextReceived += CommandManager_NextReceived;
    Player.CommandManager.PreviousReceived += CommandManager_PreviousReceived;
    
    // 建立 MediaPlaybackList 利用每一個 Item 設定 MediaItemDisplayProperties 來更新 SMTC
    MediaPlaybackList = new MediaPlaybackList();
    for (int i = 1; i < 5; i++)
    {
        MediaSource source = MediaSource.CreateFromUri(new Uri($"{Package.Current.InstalledLocation.Path}/WPFAndUWPSample/Assets/mp3/0{i}.mp3", UriKind.RelativeOrAbsolute));
        MediaPlaybackItem item = new MediaPlaybackItem(source);
        // 設定為 Music 的相關屬性
        MediaItemDisplayProperties displayProperty = item.GetDisplayProperties();
        displayProperty.Type = MediaPlaybackType.Music;
        displayProperty.MusicProperties.Title = $"0{i}.mp3";
        displayProperty.MusicProperties.AlbumArtist = "JJ";
        displayProperty.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri($"{Package.Current.InstalledLocation.Path}/WPFAndUWPSample/Assets/mp3/0{i}.jpg")); ;
        item.ApplyDisplayProperties(displayProperty);
        MediaPlaybackList.Items.Add(item);
    }
    
    Player.PlaybackList = MediaPlaybackList;
    Player.Play();
    
    更多關於 MediaPlayer 的操作可以參考 Integrate with the System Media Transport Controls。 如果您專案使用別的 Player 也希望能操作 SMTC 的話,可改用 BackgroundMediaPlayer 的方式來更新 SMTC 與注冊按鈕事件。
    private void UseBackgroundMediaPlayer()
    {
     BackgroundMediaPlayer.Current.SystemMediaTransportControls.ButtonPressed += Smtc_ButtonPressed;
    
     var updater  = BackgroundMediaPlayer.Current.SystemMediaTransportControls.DisplayUpdater;
     updater.MusicProperties.Title = "song name";
     updater.MusicProperties.AlbumArtist = "artsit and album";
     updater.Update();
    }
    
    private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
    {
     // 處理在 SMTC 操作的事件
    }

上面介紹 Win32 程式怎麽使用 UWP APIs,接著參考 Packaging a UWP application with a Win32 component in the right wayDesktop Bridge – The Migrate phase: invoking a Win32 process from a UWP app 説明,補充 UWP app 怎麽與 Win32 程式互動。

UWP app 與 Win32 App 的互動中重點:

  1. 建立一個 Windows Application Packaging Project 並把 UWP app 與 Win32 App 加入 Applicaions 集合,設定 UWP app 為起始專案;
  2. 在 Package.appxmanifest 注冊: windows.appServicewindows.fullTrustProces
    <Applications>
      <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
        <uap:VisualElements />
        <Extensions>
     <uap:Extension Category="windows.appService">
       <uap:AppService Name="com.pou.MyAppService" />
     </uap:Extension>
     <desktop:Extension Category="windows.fullTrustProcess" Executable="WPFApp\WPFApp.exe" />
        </Extensions>
      </Application>
    </Applications>
    <Capabilities>
      <Capability Name="internetClient" />
     <rescap:Capability Name="runFullTrust" />
    </Capabilities>
  3. UWP app 建立處理 AppService 的邏輯,並爲了呼叫 FullTrustProcess 需要為 UWP app 加入 Windows Desktop Extensions for the UWP
    由於 App Service 在 UWP app 安裝時會一并被安裝到系統裏面,它與 BackgroundTask 不一樣需依賴 Trigger 的機制,而是讓呼叫端利用 PackageFamilyNameAppServiceName 來指定啓動它。因此,它很適合扮演傳遞的角色;更多關於 App Service 的介紹可參考 UWP - 介紹 App Service 與新功能
    private AppServiceConnection appServiceConnection;
    private BackgroundTaskDeferral appServiceDeferral;
    
    /// AppService 在 Win10 Anniversary Update (1607) 開始支援 Sinle Process 的做法,利用 OnBackgroundActivated 來處理 AppService 的請求。
    protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
    {
        base.OnBackgroundActivated(args);
        AppServiceTriggerDetails appService = args.TaskInstance.TriggerDetails as AppServiceTriggerDetails;
    
        // appServiceDeferral 與 appServiceConnection 需要變成公用變數
        // 因爲其他時間需要用到,已維持連線的一致性
        appServiceDeferral = args.TaskInstance.GetDeferral();
        appServiceConnection = appService.AppServiceConnection;
    
        appServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
    }
    
    private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
    {
        // 當 App Service 收到請求時,該 method 就會被觸發
        // 先要求取得 取得 deferral 拉長生命周期
        var requestDeferral = args.GetDeferral();
    
        ValueSet message = args.Request.Message;
        // 抓到從 Win32 App 送來的内容,顯示在 MainPage.xaml
        string name = message["name"] as string;
        if (string.IsNullOrEmpty(name) == false && Window.Current != null)
        {
            var rootFrame = Window.Current.Content as Frame;
            if (rootFrame != null && rootFrame.Content != null)
            {
                var mainPage = rootFrame.Content as MainPage;
                mainPage.SetResponse(name);
            }
        }
    
        // 建立回傳給 Win32 的訊息
        ValueSet responseMsg = new ValueSet();
        responseMsg.Add("response", "success");
        await args.Request.SendResponseAsync(responseMsg);
    
        requestDeferral.Complete();
    }
    那麽,要怎麽從 UWP app 呼叫 Win32 app 呢?如下:
    private async void OnInvokeWin32AppClick(object sender, RoutedEventArgs e)
    {
        // 如需要傳遞參數到 Win32 app,FullTrustProcessLauncher 也有支援,不過要在 Package.appxmanifest 加入宣告
        await Windows.ApplicationModel.FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
    }
    更多詳細内容可以參考:FullTrustProcessLauncher
  4. Win32 app 利用 AppService 利用 將參數送到 UWP app;
    如果 UWP app 與 Win32 app 被封裝在同一個 Pakcage 裏面,在 Win32 app 使用 AppServiceConnection 時可用 Windows.ApplicationModel.Package.Current.Id.FamilyName 來抓取 PackageFamilyName。
    private async void OnSendToUWPClick(object sender, RoutedEventArgs e)
    {
        // 利用 Windows.ApplicationModel 的模組,要記得為 Win32 app 加入 Windows.winmd 與 Windows.Runtime 的參考
        AppServiceConnection connection = new AppServiceConnection();
        connection.AppServiceName = "com.pou.MyAppService";
        connection.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
    
        var result = await connection.OpenAsync();
        if (result == AppServiceConnectionStatus.Success)
        {
            ValueSet valueSet = new ValueSet();
            valueSet.Add("name", txtUserName.Text);
    
            var response = await connection.SendMessageAsync(valueSet);
            if (response.Status == AppServiceResponseStatus.Success)
            {
                string responseMessage = response.Message["response"].ToString();
                if (responseMessage == "success")
                {
                    this.Hide();
                }
            }
        }
    }

[補充]

[範例程式]

======

篇目的介紹 WPF 怎麽使用 Windows 10 APIs,以及怎麽與 UWP 之間互動,最後利用 Packaging Project Template 把他們包裝起來。

另外可以參考 Add Windows 10 experiences 考慮那些功能是真的需要加入到 WPF 專案裏面的。

希望幫忙想要移植現有 WPF 或是 Win32 程式的開發人員能更快評估如何開發。

References: