MAUI 初體驗 聯合 WinForm 讓家裡廢棄的手機當做電腦副品用起來

2022-11-01 18:01:28

軟體效果圖

軟體架構草圖

效果解釋:執行 winform 端後 使用 ctrl+c 先複製任何詞語,然後ctrl+空格 就可以將翻譯結果顯示在 安卓,IOS,windows 甚至 mac 任意使用者端

1:使用 VS2022 + net6 建立 MAUI 專案,建立不了的先安裝

2:建立時使用 .NET MAUI 應用,應用名字自己定義,我這裡用 MauiAppClient

3:建立後的專案圖

4:新增專案參照

CommunityToolkit.Mvvm MAUI 的官方 MVVM 庫,可以很方便的讓C#像VUE那樣簡單的使用雙向繫結
MQTTnet 這裡我們 MAUI 使用者端和伺服器端之間使用 MQTT 協定來通訊
Newtonsoft.Json 這個不解釋,不參照也可以直接使用官方的 System.Text.Json

5:開啟 MainPage.xaml 將檔案修改為一下內容,MainPage.xaml內容為APP軟體主程式視窗頁面內容

`<?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"
             x:Class="MauiAppClient.MainPage"
             xmlns:viewmodel="clr-namespace:MauiAppClient.ViewModel"
             x:DataType="viewmodel:MainPageModel"
             BackgroundColor="Black"
             >

    <ScrollView>
        <StackLayout Margin="20,35,20,25">

            <Label
                    x:Name="LabelTips"
                    Text="{Binding StrTips}"
                    FontSize="12"
                    TextColor="Red"
                    Margin="15"
                    />

            <Label
                    x:Name="LabelKey"
                    Text="{Binding StrKey}"
                    FontSize="32"
                    TextColor="GreenYellow"
                    Margin="15"
                    />

            <Label
                    x:Name="LabelValue1"
                    Text="{Binding StrValue1}"
                    FontSize="26"
                    TextColor="#08e589"
                    Margin="15"
                    />

            <Label
                    x:Name="LabelValue2"
                    Text="{Binding StrValue2}"
                    FontSize="26"
                    TextColor="#08e589"
                    Margin="15"
                    />
        </StackLayout>
    </ScrollView>

</ContentPage>
`

6:建立頁面繫結檢視模型 有不熟悉 CommunityToolkit.Mvvm 元件的同學建議先查閱下此元件的使用方式。加好模型後記得注入一下 builder.Services.AddSingleton();

    public partial class MainPageModel : ObservableObject
    {
        [ObservableProperty]
        public string strTips;

        [ObservableProperty]
        public string strKey;

        [ObservableProperty]
        public string strValue1;

        [ObservableProperty]
        public string strValue2;
    }

7:MAUI 程式主程式碼

        public static IMqttClient _mqttClient;
        public MainPageModel _vm;
        public MainPage(MainPageModel vm)
        {
            InitializeComponent();
            _vm = vm;
            BindingContext = _vm;
            MqttInit();
        }

        /// <summary>
        /// 初始化MQTT
        /// </summary>
        public void MqttInit()
        {
            string clientId = Guid.NewGuid().ToString();
            var optionsBuilder = new MqttClientOptionsBuilder()
                .WithTcpServer("xxx.xxx.xxx.xxx", 1883) // 要存取的mqtt伺服器端的 ip 和 埠號
                                                        //.WithCredentials("admin", "123456") // 要存取的mqtt伺服器端的使用者名稱和密碼
                .WithClientId(clientId) // 設定使用者端id
                .WithCleanSession()
                .WithTls(new MqttClientOptionsBuilderTlsParameters
                {
                    UseTls = false  // 是否使用 tls加密
                });

            var clientOptions = optionsBuilder.Build();
            _mqttClient = new MqttFactory().CreateMqttClient();
            _mqttClient.ConnectedAsync += _mqttClient_ConnectedAsync; // 使用者端連線成功事件
            _mqttClient.DisconnectedAsync += _mqttClient_DisconnectedAsync; // 使用者端連線關閉事件
            _mqttClient.ApplicationMessageReceivedAsync += _mqttClient_ApplicationMessageReceivedAsync; // 收到訊息事件
            _mqttClient.ConnectAsync(clientOptions);
        }

        /// <summary>
        /// 使用者端連線關閉事件
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private Task _mqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg)
        {
            _vm.StrTips = "已斷開與伺服器端的連線";
            int i = 0;
            Task.Factory.StartNew(() =>
            {
                while (_mqttClient == null || _mqttClient.IsConnected == false)
                {
                    i++;
                    Thread.Sleep(5 * 1000);
                    MqttInit();
                    _mqttClient.SubscribeAsync("pc-helper", MqttQualityOfServiceLevel.AtLeastOnce);
                    _vm.StrTips = "嘗試重新連..." + i;
                }
            }).ConfigureAwait(false);

            return Task.CompletedTask;
        }

        /// <summary>
        /// 使用者端連線成功事件
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private Task _mqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg)
        {
            _vm.StrTips = "已連線伺服器端";
            _mqttClient.SubscribeAsync("pc-helper", MqttQualityOfServiceLevel.AtLeastOnce);

            return Task.CompletedTask;
        }
        /// <summary>
        /// 收到訊息事件
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private Task _mqttClient_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg)
        {
            string msg = Encoding.UTF8.GetString(arg.ApplicationMessage.Payload);
            var model = Newtonsoft.Json.JsonConvert.DeserializeObject<MsgModel>(msg);
            _vm.StrKey = "詞語:" + model.query?.ToString();

            string value1 = "翻譯:";
            if (model.translation != null)
            {
                foreach (var item in model.translation)
                {
                    value1 += item;
                }
            }
            _vm.StrValue1 = value1;

            string value2 = "解釋:";
            if (model.basic != null && model.basic.explains != null)
            {
                foreach (var item in model.basic.explains)
                {
                    value2 += item;
                }
            }

            _vm.StrValue2 = value2;
            return Task.CompletedTask;
        }

        public void Publish(string data)
        {
            var message = new MqttApplicationMessage
            {
                Topic = "pc-helper",
                Payload = Encoding.Default.GetBytes(data),
                QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
                Retain = true  // 伺服器端是否保留訊息。true為保留,如果有新的訂閱者連線,就會立馬收到該訊息。
            };
            _mqttClient.PublishAsync(message);
        }

MAUI 釋出 APK 供手機下載安裝使用 右鍵 MAUI 專案>使用終端 輸入如下命令

keytool -genkey -v -keystore myapp.keystore -alias key -keyalg RSA -keysize 2048 -validity 10000

專案檔案中增加如下設定

<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
    <AndroidKeyStore>True</AndroidKeyStore>
    <AndroidSigningKeyStore>myapp.keystore</AndroidSigningKeyStore>
    <AndroidSigningKeyAlias>key</AndroidSigningKeyAlias>
    <AndroidSigningKeyPass></AndroidSigningKeyPass>
    <AndroidSigningStorePass></AndroidSigningStorePass>
</PropertyGroup>

釋出

dotnet publish -f:net6.0-android -c:Release /p:AndroidSigningKeyPass=youpwd /p:AndroidSigningStorePass=youpwd 替換命令中的 youpwd

參考官方 釋出 文章 https://learn.microsoft.com/zh-cn/dotnet/maui/android/deployment/publish-cli

8:建立 winform 程式 使用 .net framework 4.7 主要使用全域性快捷方式 需要用到 user32.dll 主程式程式碼

 public partial class MainForm : Form
    {
        int crtlSpace;//定義快捷鍵
        public static IMqttClient _mqttClient;
        public MainForm()
        {
            InitializeComponent();
            crtlSpace = "CtrlSpace".GetHashCode();

            //組合鍵模式 None = 0,Alt = 1,Ctrl = 2,Shift = 4,WindowsKey = 8
            Win32Api.RegisterHotKey(this.Handle, crtlSpace, 2, (int)Keys.Space);
        }

        /// <summary>
        /// 熱鍵
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m)
        {
            const int WM_HOTKEY = 0x0312;
            int wParam = (int)m.WParam;
            switch (m.Msg)
            {
                case WM_HOTKEY:
                    if (wParam == crtlSpace)
                    {
                        var text = Clipboard.GetText();
                        string result= YouDao.GetFanYiResult(text);
                        Publish(result);
                        AppentTextLog("觸發:【" + text + "】->" + System.DateTime.Now);
                    }
                    break;
            }
            base.WndProc(ref m);
        }

        /// <summary>
        /// 隱藏表單
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button_closed_Click(object sender, EventArgs e)
        {
            this.Visible = false;
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            MqttInit();
        }

        /// <summary>
        /// 視窗拖拽
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_MouseDown(object sender, MouseEventArgs e)
        {
            Win32Api.ReleaseCapture();
            Win32Api.SendMessage(this.Handle, Win32Api.WM_SYSCOMMAND, Win32Api.SC_MOVE + Win32Api.HTCAPTION, 0);
        }

        /// <summary>
        /// 雙擊右下角圖示顯示隱藏
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                this.Visible = !this.Visible;
            }
        }

        /// <summary>
        /// 右鍵右下角圖示退出程式
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void 退出ToolStripMenuItem_Click(object sender, EventArgs e)
        {
            //解除安裝註冊熱鍵
            Win32Api.UnregisterHotKey(this.Handle, this.crtlSpace);
            //關閉視窗
            this.Close();
        }

        /// <summary>
        /// 初始化MQTT
        /// </summary>
        public void MqttInit()
        {
            string clientId=Guid.NewGuid().ToString();
            var optionsBuilder = new MqttClientOptionsBuilder()
                .WithTcpServer("xxx.xxx.xxx.xxx", 1883) // 要存取的mqtt伺服器端的 ip 和 埠號
                                                        //.WithCredentials("admin", "123456") // 要存取的mqtt伺服器端的使用者名稱和密碼
                .WithClientId(clientId) // 設定使用者端id
                .WithCleanSession()
                .WithTls(new MqttClientOptionsBuilderTlsParameters
                {
                    UseTls = false  // 是否使用 tls加密
                });

            var clientOptions = optionsBuilder.Build();
            _mqttClient = new MqttFactory().CreateMqttClient();
            _mqttClient.ConnectedAsync += _mqttClient_ConnectedAsync; // 使用者端連線成功事件
            _mqttClient.DisconnectedAsync += _mqttClient_DisconnectedAsync; // 使用者端連線關閉事件
            _mqttClient.ConnectAsync(clientOptions);
        }

        /// <summary>
        /// 使用者端連線關閉事件
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private Task _mqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg)
        {
            AppentTextLog($"MQTT服務已關閉");
            return Task.CompletedTask;
        }

        /// <summary>
        /// 使用者端連線成功事件
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private Task _mqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg)
        {
            AppentTextLog($"MQTT服務已連線^v^");

            _mqttClient.SubscribeAsync("pc-helper", MqttQualityOfServiceLevel.AtLeastOnce);

            return Task.CompletedTask;
        }

        /// <summary>
        /// 傳送MQTT訊息
        /// </summary>
        /// <param name="data"></param>
        public void Publish(string data)
        {
            var message = new MqttApplicationMessage
            {
                Topic = "pc-helper",
                Payload = Encoding.UTF8.GetBytes(data),
                QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce,
                Retain = true  // 伺服器端是否保留訊息。true為保留,如果有新的訂閱者連線,就會立馬收到該訊息。
            };
            _mqttClient.PublishAsync(message);
        }

        /// <summary>
        /// 更新表單文字
        /// </summary>
        /// <param name="text"></param>
        void AppentTextLog(string text)
        {
            Action act = delegate ()
            {
                textBox_log.AppendText(Environment.NewLine + text + Environment.NewLine);
                textBox_log.ScrollToCaret();

                if (textBox_log.Text.Count() > 100000)
                {
                    textBox_log.Clear();
                }
            };
            this.Invoke(act);
        }
    }

9:翻譯API這裡使用了「有道」

        public static string GetFanYiResult(string text)
        {
            Dictionary<String, String> dic = new Dictionary<String, String>();
            string url = "https://openapi.youdao.com/api";
            string appKey = "xxx";
            string appSecret = "xxx";
            string salt = Guid.NewGuid().ToString();
            dic.Add("from", "auto");
            dic.Add("to", "zh-CHS");
            dic.Add("signType", "v3");
            TimeSpan ts = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
            long millis = (long)ts.TotalMilliseconds;
            string curtime = Convert.ToString(millis / 1000);
            dic.Add("curtime", curtime);
            string signStr = appKey + Truncate(text) + salt + curtime + appSecret;
            string sign = ComputeHash(signStr, new SHA256CryptoServiceProvider());
            dic.Add("q", text);
            dic.Add("appKey", appKey);
            dic.Add("salt", salt);
            dic.Add("sign", sign);
            return Post(url, dic);
        }

10: 到這裡兩個端都有了,就差最關鍵的 MQTT 服務了。MQTTnet 元件本身是可以起身起一個服務供軟體使用的(有興趣的同學可以自己擴充套件),這裡我們直接使用 docker 線上部署一個 mqtt 服務 (emqx,感興趣的同學可以查閱此工具,自帶web管理)

docker run -dit --restart=always -d --name emqx -e EMQX_HOST="127.0.0.1" -e EMQX_NAME="emqx"  -p 4369:4369 -p 4370:4370 -p 5369:5369 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 0.0.0.0:1883:1883 -p 0.0.0.0:18083:18083 -p 0.0.0.0:9981:8081 emqx/emqx:latest;

11 一篇文章不表述不了多少東西,也不能讓許多同學嚐鮮,這裡我放出開源地址供同學們把玩

https://gitee.com/diystring/pchelper

注意:開原始碼中的IP地址和有道的ID祕鑰都是入門級設定和有免費額度限制的,所以同學們把玩的時候不用惡搞哈,儘量讓更多的同學能下載F5嚐鮮