顯示具有 WPF 標籤的文章。 顯示所有文章
顯示具有 WPF 標籤的文章。 顯示所有文章

2022年9月6日 星期二

在 WPF 上建立與使用 Lottie 來設計出具有動畫效果

在 WPF 上建立與使用 Lottie 來設計出具有動畫效果

之前有寫過一篇 建立與使用 Lottie 來設計出具有動畫效果的 App 文章,說明如何在 .NET MAUI 專案中,使用 Lottie 檔案,產生出具有動畫效果的應用程式,這兩天聽到有個 WPF 上有這樣類似的需求,就想說是否可以在 WPF 上也能夠做出這樣的效果呢?

抱持這這樣的疑問,便開始先在網路進行搜尋,想要在 WPF 下來使用 Lottie 這樣機制的時候,究竟要使用哪個套件比較好,姊果發現到,現在可以找到的 Lottie 套件僅能夠在 .NET 5 以上的環境上運作。

而甚麼事 Lottie 呢?這是在 2015 年之後才出現的一個功能,根據維基百科上的描述可以得到這方面的說明

Lottie is a file format for vectorial animation, and is named after Charlotte Reiniger, a German pioneer of silhouette animation.

在這個 https://lottiefiles.com/ 網站上,可以看到種 Lottie 做出的動畫效果,而且可以下載到相當多的動畫 JSON 檔案

現在將來嘗試在 WPF 專案下,透過隨便下載的 Lottie JSON 檔案,將這個動畫能夠在 WPF 應用程式下來運行

建立 WPF 專案

  • 開啟 Visual Studio 2022 開發工具

  • 當 [Visual Studio 2022] 對話窗出現的時候

  • 點選右下角的 [建立新的專案] 按鈕選項

  • 現在將看到 [建立新專案] 對話窗

  • 切換右上角的 [所有專案類型] 下拉選單控制項

  • 找到並且點選 [桌面] 這個選項

  • 從清單中找到並選擇 [WPF 應用程式] 這個專案範本 (記得要選擇使用 C# 程式語言)

    此專案可用於建立 .NET WPF 應用程式

  • 點選右下角的 [下一步] 按鈕

  • 此時將會看到 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位,輸入 wpfLottie

  • 點選右下角的 [下一步] 按鈕

  • 最後會看到 [其他資訊] 對話窗

  • 使用預設設定值,也就是 [架構] 為 [.NET 6.0 (長期支援)]

  • 點選右下角的 [建立] 按鈕

加入 LottieSharp 的 NuGet 套件

  • 滑鼠右擊該專案的 [相依性] 節點
  • 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項
  • 此時,[NuGet: wpfLottie] 視窗將會出現
  • 點選 [瀏覽] 標籤頁次
  • 在左上方的搜尋文字輸入盒內輸入 LottieSharp 關鍵字
  • 若你沒有看到 8.0 以上的版本,請勾選 [包括搶鮮版] 檢查盒控制項
  • 現在,將會看到 LottieSharp 套件出現在清單內
  • 點選這個 LottieSharp 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。

下載與複製 Lottie 檔案道專案內

  • 下載完成後的檔案名稱將會為 [9945-space-launch.json]
  • 透過檔案總管拖拉這個檔案到剛剛建立的 WPF 專案根目錄下
  • 在方案總管點選這個檔案
  • 在 [屬性] 視窗內
  • 設定 [建置動作] 的屬性值為 [永遠複製]

設計使用 Lottie 元件的視窗

  • 打開 [MainWindow.xaml] 檔案
  • 將底下 XAML 宣告標記覆蓋掉這個檔案的內容
<Window x:Class="wpfLottie.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:lottie="clr-namespace:LottieSharp.WPF;assembly=LottieSharp"
       xmlns:local="clr-namespace:wpfLottie"
        mc:Ignorable="d"
        Title="Lottie 在 WPF 上的使用範例" Height="450" Width="800">
    <Grid>
        <lottie:LottieAnimationView
    Width="200"
    Height="300"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    AutoPlay="True"
    FileName="9945-space-launch.json"
    RepeatCount="-1" />
    </Grid>
</Window>

啟動與執行

  • 底下會是執行後的運作畫面

 




2022年7月29日 星期五

MAUI : 資料綁定 Data Binding - 1 自行建置 INotifyPropertyChanged 介面

資料綁定 Data Binding - 1 自行建置 INotifyPropertyChanged 介面

當在學習 毛伊 MAUI 跨平台開發工具來進行 Android , iOS , WinUI 等平台應用程式開發的時候,對於 MVVM Model View ViewModel 這樣的設計模式一定十分孰悉與精通 ,而在 MVVM 設計模式下的 資料綁定 Data Binding 觀念與實作技術,也務必要徹底了解,這樣的技術是怎麼運行起來的,可以提供甚麼樣的好處。

資料綁定 Data Binding 的技術背後將會需要實作出 INotifyPropertyChanged 介面,這個型別早就已經存在於 .NET Framework 2.0 內了,所以,這並不是甚麼十分嶄新的技術與觀念,為了要能夠體會與理解 INotifyPropertyChanged 介面,現在就實際來動手實作看看。

系列文章清單

1 自行建置 INotifyPropertyChanged 介面

2 設計基底類別,透過繼承可以使用屬性變更通知

3 PropertyChanged.Fody 套件,大幅簡化屬性變更通知程式設計碼

4 CommunityToolkit.Mvvm 套件,透過原始碼產生來簡化屬性變更通知程式設計碼

5 在 Maui 專案內,如何得知 ViewModel 內的屬性產生異動,而 View 可以收到通知呢?

實作 INotifyPropertyChanged 介面

  • 開啟 Visual Studio 2022 開發工具

  • 當 [Visual Studio 2022] 對話窗出現的時候

  • 點選右下角的 [建立新的專案] 按鈕選項

  • 現在將看到 [建立新專案] 對話窗

  • 請選擇 [主控台應用程式] 這個專案範本

  • 點選右下角的 [下一步] 按鈕

  • 此時將會看到 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位,輸入 csINotifyPropertyChanged

  • 點選右下角的 [下一步] 按鈕

  • 最後會看到 [其他資訊] 對話窗

  • 請勾選 [不要使用最上層語句] 這個文字檢查盒控制項

  • 點選右下角的 [建立] 按鈕

為了要簡化過程,因此,將會把相關程式碼都寫入到 Program.cs 這個檔案內,當然,也可以把等下要建立的新類別 [Person] 建立到新的 [Person.cs] 檔案內。

  • 請在 [Program.cs] 檔案內 namespace csINotifyPropertyChanged 這個命名空間宣告的最前面,加入一個 [Person] 類別宣告,程式碼如下:
public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

在這個 [Person] 類別內,將需要實作 INotifyPropertyChanged 這個介面,在這個 INotifyPropertyChanged 介面內,只有宣告一個 event PropertyChangedEventHandler? PropertyChanged; 成員,因此,這裡將會加入這樣的 public event PropertyChangedEventHandler? PropertyChanged; 程式碼宣告

在剛剛加入的 [PropertyChanged] 事件成員,將會用來讓外部參考與使用這個類別執行個體的其他物件,可以訂閱 [PropertyChanged] 這個事件,而當這個 [Person] 類別的內部狀態 (相關 欄位 Field) 有變動的時候,將會觸發這個 [PropertyChanged] 事件,如此,便可以讓有訂閱這個事件的其他物件,可以收到此執行個體的狀態有變更的通知,這就是 INotifyPropertyChanged 這個介面最主要的價值與目的。

為了要方便隨時可以觸發 [PropertyChanged] 事件,因此,在這裡設計了一個 [OnPropertyChanged] 方法,該方法可以接收一個字串參數,這個字串參數表示了是哪個屬性名稱發生了異動,而在這個 [OnPropertyChanged] 方法內,僅有一行敘述,這樣的寫法是為了要避免在多執行緒程式碼執行下,具有執行緒安全的特性。

好了,有了基本的 INotifyPropertyChanged 實作程式碼,現在可以來進行這個 [Person] 類別的成員設計,在這個類別內,其實僅設計兩個成員,一個是具有字串型別的 [Name] 屬性,表示這個 Person 物件的姓名,另外一個是具有整數型別的 [Age] 屬性,表示這個 Person 物件的年紀。

不過,為了能夠做出,當這兩個屬性值發生異動的時候,可以觸發 [PropertyChanged] 事件,在這裡是不能夠使用 自動實作的屬性 ,而是要使用 屬性 的方式來設計,也就是要使用 set 與 get 存取子和一個欄位成員來定義出這個屬性。

底下將會是 [Person] 這個類別內,準備要新加入的兩個屬性定義程式碼

public class Person : INotifyPropertyChanged
{

   ...

    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnPropertyChanged("Name");
        }
    }

    private int age;
    public int Age
    {
        get { return age; }
        set
        {
            age = value;
            OnPropertyChanged("Age");
        }
    }
}

在此,將先採用姓名這個屬性來說明設計過程,首先,宣告一個 欄位 成員,使用 private string name; 這樣程式碼,很清楚的,因為這是個欄位,因此,需要加入 private 修飾詞,避免外部其他物件來存取,僅供這個類別所建立的執行個體內的程式碼可以存取。

緊接著使用 含有支援欄位的屬性 來設計一個 Name 新屬性。對於這個新的 Name 屬性,其 get 存取子 將會直接回傳 name 這個欄位值,而對於 set 這個存取子,除了一定要將傳入進來的值,設定給 name 這個欄位之外,還需要使用 OnPropertyChanged("Name"); 敘述來觸發該屬性值已經發生變更的行為通知。

同樣的,也需要設計一個 Age 屬性與 age 欄位,參考上述 姓名 的做法,設計出年紀的屬性程式碼。

現在可以開始來體驗 INotifyPropertyChanged 這個介面所帶來的好處與方便性

現在,請在 Main 方法內,加入底下程式碼

Person person = new Person();
person.PropertyChanged += (s, e) =>
{
    WriteLine($"屬性 {e.PropertyName} 已經變更");
};
WriteLine("準備要修改 Name 屬性值");
person.Name = "Vulcan Lee";
WriteLine("Press any key for continuing...");
ReadKey();
WriteLine("準備要修改 Age 屬性值");
person.Age = 25;
WriteLine("Press any key for continuing...");
ReadKey();

在這裡先建立一個型別為 [Person] 的物件,指派給 person 變數內

接著,因為每個 [Person] 物件,都有個公開的 [PropertyChanged] 事件,因此,在此將需要訂閱這個事件,在此使用 Lambda 運算式 設計一個匿名委派方法來綁定這個事件,如此,當有屬性變更的事件產生的時候,將會觸發這裡綁定的 Lambda 委派方法,也就是會在螢幕上顯示出哪個屬性值已經變更了。

完成的變更屬性的事件訂閱與綁定設計,接下來就是要開始針對這個 person 物件的兩個屬性值進行變動,看看是否會有屬性變動通知事件產生,底下是執行後的結果內容。

這裡將會是整個完整的測試程式碼

global using static System.Console;
using System.ComponentModel;

namespace csINotifyPropertyChanged
{
    public class Person : INotifyPropertyChanged
    {
        #region 實作出 INotifyPropertyChanged 的程式碼
        // 在這個 INotifyPropertyChanged 介面內,只有宣告一個 event PropertyChangedEventHandler? PropertyChanged; 成員
        public event PropertyChangedEventHandler? PropertyChanged;
        // 設計底下的方法,是要方便可以觸發 PropertyChanged 這個事件
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        private int myVar;

        public int MyProperty
        {
            get { return myVar; }
            set { myVar = value; }
        }

        #region 針對每個具有 PropertyChanged 的屬性,都需要有底下的程式碼設計方式

        #region 姓名
        private string name;

        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }
        #endregion

        #region 年紀
        private int age;

        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                OnPropertyChanged("Age");
            }
        }

        #endregion
        #endregion
    }


    internal class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.PropertyChanged += (s, e) =>
            {
                WriteLine($"屬性 {e.PropertyName} 已經變更");
            };

            WriteLine("準備要修改 Name 屬性值");
            person.Name = "Vulcan Lee";

            WriteLine("Press any key for continuing...");
            ReadKey();

            WriteLine("準備要修改 Age 屬性值");
            person.Age = 25;

            WriteLine("Press any key for continuing...");
            ReadKey();
        }
    }
}






2022年7月25日 星期一

C# 非同步 : 在進行 WPF / Windows Forms / Xamarin.Forms 專案開發,可以回到 UI 執行緒來更新 UI 控制項的相同作法

 

在進行 WPF / Windows Forms / Xamarin.Forms 專案開發,可以回到 UI 執行緒來更新 UI 控制項的相同作法

在以往,若再不同 GUI 開發框架下,想要在非同步的多執行緒程式碼下,更新 UI 控制項的屬性,若當前的執行緒不是所謂的 主執行緒 或者 稱之為 UI 執行緒,將會得到 System.InvalidOperationException: '呼叫執行緒無法存取此物件,因為此物件屬於另一個執行緒。' 這樣的例外異常訊息,所以,大家第一時間想到的就是每個開發框架都會有提供這樣相對應的程式碼,可以指定一段委派方法,讓這些程式碼在 UI 執行緒下來執行,而不是在多執行緒下的某個執行緒來執行,例如:

  • Windows Forms

    這裡將會再多執行緒程式碼內,使用 Control.Invoke 方法 來做到,這個方法可以傳入一個委派方法,而該委派方法的程式碼將會在 主執行緒 或者 UI 執行緒 下來執行

  • WPF Windows Presentation Foundation

    在這個開發框架下,將會使用 Dispatcher.Invoke 方法 來做到,同樣的,這個方法可以傳入一個委派方法,而該委派方法的程式碼將會在 主執行緒 或者 UI 執行緒 下來執行

而是否有個簡單與通用的設計做法,不論在哪個開發框架下,都可以使用相同的程式設計代碼,指定一個委派方法,讓這個委派方法可以在 UI 執行緒下來執行。

建立一個 WPF 的專案

為了要說明上面描述的內容,首先,建立一個 WPF 專案

在這個 WPF 專案內找到 [MainWindow.xaml] 檔案,打開這個檔案,使用底下的 XAML 來替換

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBlock x:Name="textBlock" Text="Start"
                   FontSize="30" />
        <Button x:Name="btn" Content="OK" Width="200" Height="70"
                Click="btn_Click"
                />
    </Grid>
</Window>

在這個 XAML 檔案內,將會宣告兩個 UI 控制項,一個是 [TextBlock] ,一個是 [Button] 。

在這裡的設計情境將會是,當使用者按下這個 OK 按鈕之後,便會開始執行一個非同步的作業,在這個非同步的多執行緒程式碼中,將會建立一個迴圈,反覆執行 10 次,每次的迴圈,將會把當前的迴圈索引值,指定到 [textBlock] 這個 UI 控制項內,如此,便可以從螢幕上看到當前的迴圈索引值是多少。

在這裡將會分別採用 ThreadPool 執行緒集區 與 Task 工作來實作這個非同步之多執行緒作業,並且比較兩者的差異。

使用工作物件 Task.Run 來建立一個非同步作業

切換到 [MainWindow.xaml] 的 Code Behind 程式碼編輯視窗內,將會看到下面的程式碼

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void btn_Click(object sender, RoutedEventArgs e)
        {
        }
    }
}

現在,在 btn_Click 按鈕事件常式內,使用 Task.Run 來建立一個非同步工作物件,做到上面所提到的一個迴圈與更新迴圈索引值到畫面上的需求

將底下的程式碼加入到 btn_Click 事件內,雖然這裡使用了 await Task.Delay(1000); 這樣的敘述,但不影響整個實驗結果,當然,可以改寫成為封鎖 Block 等待 Task.Delay(1000).Wait(); 敘述。

private void btn_Click(object sender, RoutedEventArgs e)
{
    var task = Task.Run(async () =>
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(1000);
            textBlock.Text = i.ToString();
        }
    });
}

現在,可以執行這個 WPF 專案,一旦應用程式啟動成功後,點選視窗中的 OK 按鈕

沒意外的話,應用程式沒有當掉,並且螢幕也沒有任何變化,為什麼會是這樣的情況呢?

若讀者對於 非同步工作 Task 這個類別夠孰悉的話,將會知道,這個工作物件在非同執行所傳入的委派方法的時候,若該委派方法產生了例外異常,這個工作執行個體將會蒐集當時的例外異常資訊,並且不會讓整個應用程式造成崩潰,這也就是為什麼會有這樣的執行結果了。

若你對於 非同步工作 Task 不太孰悉,建議可以來參加我開設的 精準解析 .NET Task 工作 課程

現在,將 btn_Click 這個事件委派方法內,使用 執行緒 集區來建立一個非同步多執行緒作業,看看剛剛用於 Task 物件內的相同委派方法程式碼,在執行緒下會有甚麼結果呢?

請將底下程式碼替換掉 btn_Click 事件內的程式碼

private void btn_Click(object sender, RoutedEventArgs e)
{
    ThreadPool.QueueUserWorkItem(async _ =>
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(1000);
            textBlock.Text = i.ToString();
        }
    });
}

讓我們來執行這個專案,並且按下 OK 按鈕,看看會有甚麼情況發生。

很不幸的,當按下 OK 按鈕的之後,這個應用程式崩潰了,Visual Studio 2022 出現底下的畫面

Visual Studio 2022

例外異常的訊息文字為

System.InvalidOperationException: '呼叫執行緒無法存取此物件,因為此物件屬於另一個執行緒。'

從這裡的錯誤訊息可以得知,在 GUI 應用程式下,例如 Windows Forms , WPF , Xamarin.Forms 開發框架下, 想要修改 UI 控制項的屬性,該段程式碼必須要在 UI 執行緒下來執行,否則,就會造成上面的錯誤與得到上面的錯誤訊息。

為了要解決這個問題,在這裡將會採用一種做法,這種作法將會適用於不同 UI 開發框架下,皆可使用。

這個方法就是採用 同步內容 SynchronizationContext ,請將底下程式碼替換掉 btn_Click 方法內的程式碼

private void btn_Click(object sender, RoutedEventArgs e)
{
    #region 記錄下當前的 SynchronizationContext
    SynchronizationContext context = SynchronizationContext.Current;
    #endregion
    ThreadPool.QueueUserWorkItem(async _ =>
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(1000);
            #region 透過同步內容來更新 UI 屬性
            context.Post(_ =>
            {
               textBlock.Text = i.ToString();
            }, null);
            #endregion
        }
    });
}

上面的程式碼會在使用者點選 OK 按鈕之後開始執行,首先,使用 SynchronizationContext.Current 取得當前的 SynchronizationContext 物件,從微軟文件上得知,這個類別提供在各種同步處理模式中散佈同步處理內容的基本功能,而這個物件內有個 SynchronizationContext.Post(SendOrPostCallback, Object) 方法 ,其會將非同步訊息分派至同步處理內容。

了解完成 SynchronizationContext 這個物件用法,可以來執行這個專案,將會看到底下的正確無誤的執行過程。

因此,若想要使用單一做法,在不同 UI 開發框架下做到非同步多執行緒程式碼,可以順利更新 UI 控制項內容,可以使用 SynchronizationContext 來嘗試看看。