2022年9月29日 星期四

在 .NET MAUI 專案內進行讀取 QR Code 二維條碼內容

在 .NET MAUI 專案內進行讀取 QR Code 二維條碼內容

對於如何透過手機 App 來讀取 QRCode 二維條碼這樣的需求,向來是要學習手機 App 開發學員的前三名最想要知道的技術,在這篇文章中,將會來說明如何透過手機鏡頭,讀取 QR Code 二維條碼上的內容是甚麼?

建立 .NET MAUI 應用程式 專案

  • 開啟 Visual Studio 2022 版本

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

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

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

  • 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本

    此專案可用於建立適用於 iOS、Android、Mac Catalyst、Tizen和WinUI 的 .NET MAUI 應用程式

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

  • 當出現了 [設定新的專案] 對話窗

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

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

  • 當出現了 [其他資訊] 對話窗

  • 對於 [架構] 的下拉選單控制項,使用預設值

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

加入 PropertyChanged.Fody 的 NuGet 套件

  • 滑鼠右擊該專案的 [相依性] 節點
  • 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項
  • 此時,[NuGet: mauiScanQRCode] 視窗將會出現
  • 點選 [瀏覽] 標籤頁次
  • 在左上方的搜尋文字輸入盒內輸入 ZXing.Net.Maui 關鍵字
  • 現在,將會看到 ZXing.Net.Maui 套件出現在清單內
  • 點選這個 ZXing.Net.Maui 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。

進行 ZXing.NET.Maui 套件的服務註冊

  • 在專案根目錄下
  • 找到並且打開 MauiProgram.cs 檔案
  • 找到 .UseMauiApp<App>() 程式碼
  • 在其下方加入 .UseBarcodeReader() Fluent 呼叫程式碼

對 Android 專案進行需要用到權限宣告

  • 在專案內,找到 Platforms\Android 目錄
  • 在其目錄內找到並且打開 MainApplication.cs 檔案
  • 找到 namespace 關鍵字
  • 在其上方加入 [assembly: UsesPermission(Android.Manifest.Permission.Camera)] 敘述,說明這個 Android App 需要使用到 Camera 這個權限,如此,才可以讓這個 Zxing 軟體透過鏡頭來讀取到條碼的圖片

對 Android 專案進行需要用到權限宣告

  • 在專案內,找到 Platforms\iOS 目錄
  • 找到 Info.plist 檔案,並且選擇使用 XML (文字) 編輯器工具,打開這個檔案
  • 找到 </dict> 這個關鍵字
  • 在其上方,加入底下的 XML 宣告
<key>NSCameraUsageDescription</key>
<string>This app uses barcode scanning to...</string>

將 QR Code 讀取檢視控制項放到 XAML 頁面內

  • 在專案根目錄下
  • 找到並且打開 MainPage.xaml 這個檔案
  • 使用底下 XAML 內容將其替換掉
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI"
             x:Class="mauiScanQRCode.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

            <Label x:Name="labelResult"
                   FontSize="24" TextColor="Red"/>
            
            <zxing:CameraBarcodeReaderView
                x:Name="cameraBarcodeReaderView"
                BarcodesDetected="BarcodesDetected" />
            
            <Button
                x:Name="CounterBtn"
                Text="Click me"
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

在上面的 XAML 頁面宣告中,首先加入了一個新的 zxing 命名空間宣告 : xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI" ,如此,在這個頁面中,便可以透過這個 zxing 這個前置詞,存取到 ZXing.Net.MAUI 這個套件所提供的功能與檢視了。

在 Button 按鈕前,加入了 <zxing:CameraBarcodeReaderView x:Name="cameraBarcodeReaderView" BarcodesDetected="BarcodesDetected" /> 這個 CameraBarcodeReaderView 檢視控制項,這個控制項將會把鏡頭看到的影像,顯示到螢幕上,並且會針對每個讀取進來的影像,進行條碼分析,一旦發現到有條碼圖片存在,並且該條碼也被解碼了,將會呼叫觸發這個 BarcodesDetected 事件,並且可以透該事件內提供的事件參數,取得所掃描到的條碼內容。

由於是採用事件觸發方式來設計,所以,需要透過 Code Behind 的方式來設計相關商業邏輯

  • 找到並且打開 MainPage.xaml.cs 檔案
  • 使用底下 C# 程式碼來替換掉這個檔案內容
using ZXing.Net.Maui;

namespace mauiScanQRCode;

public partial class MainPage : ContentPage
{
	int count = 0;

	public MainPage()
	{
		InitializeComponent();

        cameraBarcodeReaderView.Options = new BarcodeReaderOptions
        {
            Formats = BarcodeFormats.All,
            AutoRotate = true,
            Multiple = true
        };
    }

	private void OnCounterClicked(object sender, EventArgs e)
	{
		count++;

		if (count == 1)
			CounterBtn.Text = $"Clicked {count} time";
		else
			CounterBtn.Text = $"Clicked {count} times";

		SemanticScreenReader.Announce(CounterBtn.Text);
	}

	private void BarcodesDetected(object sender, ZXing.Net.Maui.BarcodeDetectionEventArgs e)
	{
		string Result = "";
        foreach (var barcode in e.Results)
            Result += $"Barcodes: {barcode.Format} -> {barcode.Value}  ";

		MainThread.BeginInvokeOnMainThread(() =>
		{
			labelResult.Text = Result;
		});
    }
}

在這個頁面建構式內,首先建立一個 BarcodeReaderOptions 物件,用來宣告這個 QRCode 的掃描行為,這裡使用了 Formats 屬性,宣告要掃描所有類型的一維或者二維條碼圖片,使用 AutoRotate 將會允許旋轉,使用 Multiple 屬性,宣告可以讀取多個條碼內容。完成之後,設定到這個 QR Code 讀取元件內的 cameraBarcodeReaderView.Options 屬性內。

對於一旦條碼讀取到之後,將會觸發這裡的事件委派方法,其中,該事件傳遞進來的參數將會是 ZXing.Net.Maui.BarcodeDetectionEventArgs 型別,由於上面宣告了可以一次讀取一個以上的條碼圖片,因此,在 e.Results 將會有可能擁有多筆條碼內容,所以,在此使用迴圈,將所有讀到的條碼內容,都放到 Result 這個變數內。

當蒐集到所有的條碼內容之後,便可以將這些文字內容,顯示到螢幕上,這裡使用了 labelResult.Text = Result; 這樣的敘述,不過,因為這個觸發所執行的事件方法,不會在 UI (或 主) 執行緒上運行,為了要能夠更新 labelResult 這個 UI 元件的 Text 屬性值,所以,需要透過 MainThread.BeginInvokeOnMainThread 敘述,讓這些程式碼能夠在主執行緒上來運行。

執行與測試

底下影片將會是在實體手機上的執行結果

 





2022年9月22日 星期四

在 .NET C# 下使用 MQTT 來訂閱與發佈訊息

在 .NET C# 下使用 MQTT 來訂閱與發佈訊息

對於 MQTT 的定義,在維基百科上是這麼說的:訊息佇列遙測傳輸(英語: Message Queuing Telemetry Transport,MQTT )是ISO 標準(ISO/IEC PRF 20922)下基於發布 (Publish)/訂閱 (Subscribe)範式的訊息協定,可視為「資料傳遞的橋梁」它工作在 TCP/IP協定族上,是為硬體效能低下的遠端裝置以及網路狀況糟糕的情況下而設計的發布/訂閱型訊息協定,為此,它需要一個訊息中介軟體,以解決當前繁重的資料傳輸協定,如:HTTP。

首先, MQTT 是 Message Queuing Telemetry Transport 的縮寫,從字面意義來看,就是把要傳遞的訊息放在一個佇列內,接著可以傳送 Publish 到別的地方,而需要接收到這個訊息的用戶,則是需要訂閱這主題類型的訊息,便可以收到來自遠端傳送過來的訊息,所以,上面解釋文章有提到:可視為「資料傳遞的橋梁」,也就是這樣的意思。

在這裡將會使用 MQTTNet 這個套件來進行操作,看看如何在分散式的環境中,進行資料傳遞操作。

建立測試用 MQTT 伺服器端 的主控台應用程式專案

  • 打開 Visual Studio 2022
  • 點選右下方的 [建立新的專案] 按鈕
  • 選擇一個 [主控台應用程式] 的專案範本
  • 點選右下方的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗內,在 [專案名稱] 欄位中,輸入 csMqttServer
  • 點選右下方的 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 取消 [Do not use top-level statements] 這個 checkbox 檢查盒的勾選
  • 點選右下方的 [建立] 按鈕
  • 透過 NuGet 工具,搜尋到 MQTTnet 這個套件,將其安裝到這個專案
  • 打開 Program.cs 檔案,將底下內容替換掉這個檔案內容
using MQTTnet.Server;
using MQTTnet;
using System.Text;

namespace csMqttServer;

internal class Program
{
    private static IMqttServer _mqttServer;

    static void Main(string[] args)
    {

        // 設定 MQTT server 的運作環境參數
        var optionsBuilder = new MqttServerOptionsBuilder()
            .WithConnectionBacklog(100)
            .WithDefaultEndpointPort(1884)
            .WithMaxPendingMessagesPerClient(1000);

        // 透過 MqttFactory 這個工廠方法類別,建立一個 MQTT 伺服器物件
        _mqttServer = new MqttFactory().CreateMqttServer();

        // 綁定當訊息送到此伺服器之後的事件,定義該做甚麼事情
        _mqttServer.UseApplicationMessageReceivedHandler(async e =>
        {
            // 對於此訊息的 Payload 部分,將會是經過編碼的,因此需要先解碼
            var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
            var topic = e.ApplicationMessage.Topic;
            if (topic == "Hi")
            {
                Console.WriteLine("subscription message received");
                Console.WriteLine("Simulating Say Hello messages...");
                await SimulateSayHelloAsync();
            }
        });

        // 一旦有用戶端連線上來之後,將會觸發這個事件
        _mqttServer.UseClientConnectedHandler(e =>
        {
            Console.WriteLine("***** CLIENT CONNECTED : " + e.ClientId + " *******");
        });

        // 啟動這個 MQTT 伺服器
        _mqttServer.StartAsync(optionsBuilder.Build());

        Console.ReadLine();
    }

    private static async Task SimulateSayHelloAsync()
    {
        await PublishMessage("HiEcho", "Hello Word!");
    }

    private static async Task PublishMessage(string topic, string message)
    {
        // 產生一個 MQTT 訊息
        var mqttMessage = new MqttApplicationMessageBuilder()
                            .WithTopic(topic)
                            .WithPayload(message)
                            .WithAtLeastOnceQoS()
                            .WithRetainFlag(false)
                            .WithDupFlag(false)
                            .Build();

        // 發佈此剛剛建立的訊息
        var result = await _mqttServer.PublishAsync(mqttMessage, CancellationToken.None);

        if (result.ReasonCode == MQTTnet.Client.Publishing.MqttClientPublishReasonCode.Success)
            Console.WriteLine("Message published : " + message);
    }
}

這是一個 MQTT 伺服器端的應用程式,這個程式最後將會 Console.ReadLine(); 敘述,除非使用者在命令提示字元視窗內按下 Enter 按鍵,否則這個 MQTT 伺服器會一直持續執行中。

首先建立一個 MqttServerOptionsBuilder 物件,這是要用來設定 MQTT server 的運作環境參數,接著使用 new MqttFactory().CreateMqttServer() 來建立一個 MQTT 伺服器的物件。

一旦取得這個伺服器物件之後,緊接著便可以進行一些事件綁定的程式碼設計,首先先使用 UseApplicationMessageReceivedHandler 這個事件綁定到一個委派方法內,用來定義當訊息送到此伺服器之後的事件,定義該做甚麼事情。

在這個練習中,將會判斷當伺服器收到的訊息之主題為 Hi 這個名稱,便會在這個方法內呼叫 await SimulateSayHelloAsync(); 敘述,對送出此訊息的用戶端,送出回應訊息。

綁定 MQTT 伺服器上的第二個事件為 UseClientConnectedHandler ,當有用戶端成功連線到這台伺服器上之後,便會出發這個事件,在此將會僅僅顯示一個訊息到螢幕上,當然,在實際用途上,可以做出其他層面的不同應用設計。

最後要來看看 PublishMessage 這個方法,如何在伺服器上,對於用戶端發送訊息,這個方法將會被 SimulateSayHelloAsync 所呼叫,而此方法將會傳送 Topic 主題與 Message 訊息內容物件過來,首先會先透過建立 MqttApplicationMessageBuilder 物件,指定要傳送訊息的主題、內容、與其他相關傳遞參數,接著呼叫 MQTT 伺服器物件上的 PublishAsync 方法,將剛剛建立好的 MqttApplicationMessage 物件,送出到有訂閱該主題的用戶端上。

啟動與執行該 MQTT 伺服器專案

若沒有問題,可以執行上面寫的專案,讓這個 MQTT 伺服器可以運作起來。

建立測試用 MQTT 用戶端 的主控台應用程式專案

  • 打開 Visual Studio 2022
  • 點選右下方的 [建立新的專案] 按鈕
  • 選擇一個 [主控台應用程式] 的專案範本
  • 點選右下方的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗內,在 [專案名稱] 欄位中,輸入 csMqttClient
  • 點選右下方的 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 取消 [Do not use top-level statements] 這個 checkbox 檢查盒的勾選
  • 點選右下方的 [建立] 按鈕
  • 透過 NuGet 工具,搜尋到 MQTTnet 這個套件,將其安裝到這個專案
  • 打開 Program.cs 檔案,將底下內容替換掉這個檔案內容
using MQTTnet.Client.Options;
using MQTTnet.Client;
using MQTTnet;
using System.Text;
using MQTTnet.Client.Subscribing;

namespace csMqttClient;

internal class Program
{
    private static IMqttClient _mqttClient;

    static void Main(string[] args)
    {
        // 使用 MqttFactory 工廠類別,建立一個 MQTT 用戶端的物件
        _mqttClient = new MqttFactory().CreateMqttClient();

        // 每個用戶端都要有唯一的 Id,並且要再宣告 MQTT 伺服器的連線位址與方式
        var options = new MqttClientOptionsBuilder().WithClientId("MqttClient")
                                                    .WithTcpServer("localhost", 1884)
                                                    .Build();

        // 綁定當此用戶端已經連線到遠端伺服器之後,所要處理的工作
        _mqttClient.UseConnectedHandler(async e =>
        {
            // 在此訂閱有興趣的主題訊息
            MqttClientSubscribeResult subResult =
            await _mqttClient.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
            .WithTopicFilter("HiEcho")
            .Build());

            // 對伺服器送出問好訊息
            PublishMessage("Hi", "Test Message");
        });

        // 綁定當訊息傳送到此用戶端之後,將會觸發的事件
        _mqttClient.UseApplicationMessageReceivedHandler(e =>
        {
            Console.WriteLine($"來自伺服器的問安 {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
        });

        // 連線到遠端伺服器上
        _mqttClient.ConnectAsync(options, CancellationToken.None);

        Console.Read();
    }

    private static async void PublishMessage(string topic, string message)
    {
        // Create mqttMessage
        var mqttMessage = new MqttApplicationMessageBuilder()
                            .WithTopic(topic)
                            .WithPayload(message)
                            .WithExactlyOnceQoS()
                            .Build();

        // Publish the message asynchronously
        await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None);
    }
}

現在要來設計 MQTT 用戶端的應用程式,這裡使用 new MqttFactory().CreateMqttClient(); 敘述建立一個 _mqttClient 物件,接著透過 new MqttClientOptionsBuilder().WithClientId("MqttClient").WithTcpServer("localhost", 1884).Build(); 敘述指定該 MQTT 用戶端的 ID 與要連線到後端 MQTT 伺服器上的連線資訊,如此,將會得到一個 IMqttClientOptions 型別的物件。

現在要透過呼叫 PublishMessage 自訂方法來將這個 MQTT 訊息傳送出去。

建立一個 MqttApplicationMessageBuilder 物件,指定要送出訊息內容與該訊息搭配的 Topic,便可以使用 await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None); 敘述將這個訊息送到遠端伺服器上

啟動與執行該 MQTT 用戶端專案

若沒有問題,可以執行上面寫的專案,讓這個 MQTT 伺服器可以運作起來。

執行結果

從底下兩個主控台螢幕視窗輸入內容,可以看到這兩個專案的執行結果

這是 MQTT 伺服器執行的輸出內容

這是 MQTT 用戶端執行的輸出內容