什么是maui
.NET 多平臺應用 UI (.NET MAUI) 是一個跨平臺框架,用于使用 C# 和 XAML 創建本機移動(ios,andriod)和桌面(windows,mac)應用。

chagpt
最近這玩意很火,由于網頁版本限制了ip,還得必須開代理, 用起來比較麻煩,所以我嘗試用maui開發一個聊天小應用 結合 chatgpt的開放api來實現(很多客戶端使用網頁版本接口用cookie的方式,有很多限制(如下圖)總歸不是很正規)

效果如下
mac端由于需要升級macos13才能開發調試,這部分我還沒有完成,不過maui的控件是跨平臺的,放在后續我升級系統再說
本項目開源
https://github.com/yuzd/maui_chatgpt
學習maui的老鐵支持給個star
開發實戰
我是設想開發一個類似jetbrains的ToolBox應用一樣,啟動程序在桌面右下角出現托盤圖標,點擊圖標彈出應用(風格在windows mac平臺保持一致)
需要實現的功能一覽
托盤圖標(右鍵點擊有menu)
webview(js和csharp互相調用)
聊天SPA頁面(react開發,build后讓webview展示)
新建一個maui工程(vs2022)

坑一:默認編譯出來的exe是直接雙擊打不開的

工程文件加上這個配置
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained?Condition="'$(IsUnpackaged)'?==?'true'">true</WindowsAppSDKSelfContained>
<SelfContained?Condition="'$(IsUnpackaged)'?==?'true'">true</SelfContained>
以上修改后,編譯出來的exe雙擊就可以打開了
托盤圖標(右鍵點擊有menu)
啟動時設置窗口不能改變大小,隱藏titlebar, 讓Webview控件占滿整個窗口

這里要根據平臺不同實現不同了,windows平臺采用winAPI調用,具體看工程代碼吧
WebView
在MainPage.xaml 添加控件

對應的靜態html等文件放在工程的 Resource\Raw文件夾下 (整個文件夾里面默認是作為內嵌資源打包的,工程文件里面的如下配置起的作用)
<!--?Raw?Assets?(also?remove?the?"Resources\Raw"?prefix)?-->
<MauiAsset?Include="Resources\Raw\**"?LogicalName="%(RecursiveDir)%(Filename)%(Extension)"?/>

【重點】js和csharp互相調用
這部分我找了很多資料,最終參考了這個demo,然后改進了下
https://github.com/mahop-net/Maui.HybridWebView
主要原理是:
js調用csharp方法前先把數據存儲在localstorage里
然后windows.location切換特定的url發起調用,返回一個promise,等待csharp的事件
csharp端監聽webview的Navigating事件,異步進行下面處理
根據url解析出來localstorage的key
然后csharp端調用excutescript根據key拿到localstorage的value
進行邏輯處理后返回通過事件分發到js端
js的調用封裝如下:
//?調用csharp的方法封裝
export?default?class?CsharpMethod?{constructor(command,?data)?{this.RequestPrefix?=?"request_csharp_";this.ResponsePrefix?=?"response_csharp_";//?唯一this.dataId?=?this.RequestPrefix?+?new?Date().getTime();//?調用csharp的命令this.command?=?command;//?參數this.data?=?{?command:?command,?data:?!data???''?:?JSON.stringify(data),?key:?this.dataId?}}//?調用csharp?返回promisecall()?{//?把data存儲到localstorage中?目的是讓csharp端獲取參數localStorage.setItem(this.dataId,?this.utf8_to_b64(JSON.stringify(this.data)));let?eventKey?=?this.dataId.replace(this.RequestPrefix,?this.ResponsePrefix);let?that?=?this;const?promise?=?new?Promise(function?(resolve,?reject)?{const?eventHandler?=?function?(e)?{window.removeEventListener(eventKey,?eventHandler);let?resp?=?e.newValue;if?(resp)?{//?從base64轉換let?realData?=?that.b64_to_utf8(resp);if?(realData.startsWith('err:'))?{reject(realData.substr(4));}?else?{resolve(realData);}}?else?{reject("unknown error :?"?+?eventKey);}};//?注冊監聽回調(csharp端處理完發起的)window.addEventListener(eventKey,?eventHandler);});//?改變location?發送給csharp端window.location?=?"/api/"?+?this.dataId;return?promise;}//?轉成base64?解決中文亂碼utf8_to_b64(str)?{return?window.btoa(unescape(encodeURIComponent(str)));}//?從base64轉過來?解決中文亂碼b64_to_utf8(str)?{return?decodeURIComponent(escape(window.atob(str)));}}
前端的使用方式
import?CsharpMethod?from?'../../services/api'//?發起調用csharp的chat事件函數
const?method?=?new?CsharpMethod("chat",?{msg:?message});
method.call()?//?call返回promise
.then(data?=>{//?拿到csharp端的返回后展示onMessageHandler({message:?data,username:?'Robot',type:?'chat_message'});
}).catch(err?=>??{alert(err);
});
csharp端的處理:

這么封裝后,js和csharp的互相調用就很方便了
chatgpt的開放api調用
注冊號chatgpt后可以申請一個APIKEY

API封裝:
public?static?async?Task<CompletionsResponse>?GetResponseDataAsync(string?prompt){//?Set?up?the?API?URL?and?API?keystring?apiUrl?=?"https://api.openai.com/v1/completions";//?Get?the?request?body?JSONdecimal?temperature?=?decimal.Parse(Setting.Temperature,?CultureInfo.InvariantCulture);int?maxTokens?=?int.Parse(Setting.MaxTokens,?CultureInfo.InvariantCulture);string?requestBodyJson?=?GetRequestBodyJson(prompt,?temperature,?maxTokens);//?Send?the?API?request?and?get?the?response?datareturn?await?SendApiRequestAsync(apiUrl,?Setting.ApiKey,?requestBodyJson);}private?static?string?GetRequestBodyJson(string?prompt,?decimal?temperature,?int?maxTokens){//?Set?up?the?request?bodyvar?requestBody?=?new?CompletionsRequestBody{Model?=?"text-davinci-003",Prompt?=?prompt,Temperature?=?temperature,MaxTokens?=?maxTokens,TopP?=?1.0m,FrequencyPenalty?=?0.0m,PresencePenalty?=?0.0m,N?=?1,Stop?=?"[END]",};//?Create?a?new?JsonSerializerOptions?object?with?the?IgnoreNullValues?and?IgnoreReadOnlyProperties?properties?set?to?truevar?serializerOptions?=?new?JsonSerializerOptions{IgnoreNullValues?=?true,IgnoreReadOnlyProperties?=?true,};//?Serialize?the?request?body?to?JSON?using?the?JsonSerializer.Serialize?method?overload?that?takes?a?JsonSerializerOptions?parameterreturn?JsonSerializer.Serialize(requestBody,?serializerOptions);}private?static?async?Task<CompletionsResponse>?SendApiRequestAsync(string?apiUrl,?string?apiKey,?string?requestBodyJson){//?Create?a?new?HttpClient?for?making?the?API?requestusing?HttpClient?client?=?new?HttpClient();//?Set?the?API?key?in?the?request?headersclient.DefaultRequestHeaders.Add("Authorization",?"Bearer?"?+?apiKey);//?Create?a?new?StringContent?object?with?the?JSON?payload?and?the?correct?content?typeStringContent?content?=?new?StringContent(requestBodyJson,?Encoding.UTF8,?"application/json");//?Send?the?API?request?and?get?the?responseHttpResponseMessage?response?=?await?client.PostAsync(apiUrl,?content);//?Deserialize?the?responsevar?responseBody?=?await?response.Content.ReadAsStringAsync();//?Return?the?response?datareturn?JsonSerializer.Deserialize<CompletionsResponse>(responseBody);}
調用方式
var?reply?=?await?ChatService.GetResponseDataAsync('xxxxxxxxxx');
完整代碼參考 https://github.com/yuzd/maui_chatgpt
在學習maui的過程中,遇到問題我在microsoft learn提問,回答的效率很快,推薦大家試試看

關于我

微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。27年來,世界各地的技術社區領導者,因其在線上和線下的技術社區中分享專業知識和經驗而獲得此獎項。
MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社區投入極大的熱情并樂于助人的專家。MVP致力于通過演講、論壇問答、創建網站、撰寫博客、分享視頻、開源項目、組織會議等方式來幫助他人,并最大程度地幫助微軟技術社區用戶使用Microsoft技術。
更多詳情請登錄官方網站https://mvp.microsoft.com/zh-cn