NewLife/NewLife.Remoting

[feat]增加WinForm客户端例程,主要为了测试同步调用异步方法等场景
大石头 authored at 2024-11-23 22:58:12
a350c10
Tree
1 Parent(s) a1f740f
Summary: 10 changed files with 755 additions and 1 deletions.
Modified +7 -0
Modified +1 -1
Added +79 -0
Added +24 -0
Added +37 -0
Added +139 -0
Added +238 -0
Added +120 -0
Added +73 -0
Added +37 -0
Modified +7 -0
diff --git a/NewLife.Remoting.sln b/NewLife.Remoting.sln
index 3bb1f98..f06905b 100644
--- a/NewLife.Remoting.sln
+++ b/NewLife.Remoting.sln
@@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTZero", "Samples\IoTZero\
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroServer", "Samples\ZeroServer\ZeroServer.csproj", "{95AA14E4-6771-487B-886E-251C785624E6}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zero.Desktop", "ZeroClient\Zero.Desktop.csproj", "{C9E9BE11-9B06-483D-86C1-3C11D1A5907A}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -68,6 +70,10 @@ Global
 		{95AA14E4-6771-487B-886E-251C785624E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{95AA14E4-6771-487B-886E-251C785624E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{95AA14E4-6771-487B-886E-251C785624E6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C9E9BE11-9B06-483D-86C1-3C11D1A5907A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C9E9BE11-9B06-483D-86C1-3C11D1A5907A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C9E9BE11-9B06-483D-86C1-3C11D1A5907A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C9E9BE11-9B06-483D-86C1-3C11D1A5907A}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -77,6 +83,7 @@ Global
 		{8351AC88-1955-48AD-B6F4-26959E1A0C4C} = {8DB8495C-1EB6-41AE-83DD-55DBBB0E6FA2}
 		{1DEAF969-F093-4D99-80C9-1CEA3BD06ABB} = {8DB8495C-1EB6-41AE-83DD-55DBBB0E6FA2}
 		{95AA14E4-6771-487B-886E-251C785624E6} = {8DB8495C-1EB6-41AE-83DD-55DBBB0E6FA2}
+		{C9E9BE11-9B06-483D-86C1-3C11D1A5907A} = {8DB8495C-1EB6-41AE-83DD-55DBBB0E6FA2}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {323831A1-A95B-40AB-B9AD-36A0BC10C2CB}
Modified +1 -1
diff --git a/NewLife.Remoting/NewLife.Remoting.csproj b/NewLife.Remoting/NewLife.Remoting.csproj
index ce8f1d1..b51f91a 100644
--- a/NewLife.Remoting/NewLife.Remoting.csproj
+++ b/NewLife.Remoting/NewLife.Remoting.csproj
@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFrameworks>net45;net461;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
+    <TargetFrameworks>net45;net461;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
     <AssemblyTitle>协议通信库</AssemblyTitle>
     <Description>提供高性能RPC客户端服务端,提供Http/WebSocket客户端服务端,提供应用级客户端</Description>
     <Company>新生命开发团队</Company>
Added +79 -0
diff --git a/ZeroClient/app.manifest b/ZeroClient/app.manifest
new file mode 100644
index 0000000..2255841
--- /dev/null
+++ b/ZeroClient/app.manifest
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+    <security>
+      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+        <!-- UAC 清单选项
+             如果想要更改 Windows 用户帐户控制级别,请使用
+             以下节点之一替换 requestedExecutionLevel 节点。
+
+        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
+        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
+        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
+
+            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
+            如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
+            元素。
+        -->
+        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
+           Windows 版本的列表。取消评论适当的元素,
+           Windows 将自动选择最兼容的环境。 -->
+
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
+
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
+
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
+
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
+
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+
+    </application>
+  </compatibility>
+
+  <!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
+       自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
+       选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
+       在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
+       
+       将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
+  <!--
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
+      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
+    </windowsSettings>
+  </application>
+  -->
+
+  <!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
+  <!--
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+          type="win32"
+          name="Microsoft.Windows.Common-Controls"
+          version="6.0.0.0"
+          processorArchitecture="*"
+          publicKeyToken="6595b64144ccf1df"
+          language="*"
+        />
+    </dependentAssembly>
+  </dependency>
+  -->
+
+</assembly>
Added +24 -0
diff --git a/ZeroClient/appsettings.json b/ZeroClient/appsettings.json
new file mode 100644
index 0000000..368e81f
--- /dev/null
+++ b/ZeroClient/appsettings.json
@@ -0,0 +1,24 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  },
+  "AllowedHosts": "*",
+  //"StarServer": "http://s.newlifex.com:6600",
+  //"RedisCache": "server=127.0.0.1:6379;password=;db=3",
+  //"RedisQueue": "server=127.0.0.1:6379;password=;db=5",
+  "ConnectionStrings": {
+    "Membership": "Data Source=..\\Data\\Membership.db;Provider=SQLite",
+    "Log": "Data Source=..\\Data\\Log.db;Provider=SQLite",
+    "Zero": "Data Source=..\\Data\\Zero.db;Provider=SQLite"
+
+    // 各种数据库连接字符串模版,连接名Zero对应Zero.Data/Projects/Model.xml中的ConnName
+    //"Zero": "Server=.;Port=3306;Database=zero;Uid=root;Pwd=root;Provider=MySql",
+    //"Zero": "Data Source=.;Initial Catalog=zero;user=sa;password=sa;Provider=SqlServer",
+    //"Zero": "Server=.;Database=zero;Uid=root;Pwd=root;Provider=PostgreSql",
+    //"Zero": "Data Source=Tcp://127.0.0.1/ORCL;User Id=scott;Password=tiger;Provider=Oracle"
+  }
+}
Added +37 -0
diff --git a/ZeroClient/ClientSetting.cs b/ZeroClient/ClientSetting.cs
new file mode 100644
index 0000000..7959ffb
--- /dev/null
+++ b/ZeroClient/ClientSetting.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel;
+using NewLife;
+using NewLife.Configuration;
+using NewLife.Remoting.Clients;
+
+namespace Zero.Desktop;
+
+[Config("ClientSetting")]
+public class ClientSetting : Config<ClientSetting>, IClientSetting
+{
+    #region 属性
+    /// <summary>语音提示。默认true</summary>
+    [Description("语音提示。默认true")]
+    public Boolean SpeechTip { get; set; } = true;
+
+    /// <summary>证书</summary>
+    [Description("证书")]
+    public String Code { get; set; }
+
+    /// <summary>密钥</summary>
+    [Description("密钥")]
+    public String Secret { get; set; }
+
+    /// <summary>服务地址端口。默认为空,子网内自动发现</summary>
+    [Description("服务地址端口。默认为空,子网内自动发现")]
+    public String Server { get; set; } = "";
+    #endregion
+
+    #region 加载/保存
+    protected override void OnLoaded()
+    {
+        if (Server.IsNullOrEmpty()) Server = "http://s.newlifex.com:6600";
+
+        base.OnLoaded();
+    }
+    #endregion
+}
\ No newline at end of file
Added +139 -0
diff --git a/ZeroClient/FrmMain.cs b/ZeroClient/FrmMain.cs
new file mode 100644
index 0000000..de453b7
--- /dev/null
+++ b/ZeroClient/FrmMain.cs
@@ -0,0 +1,139 @@
+using System.Reflection;
+using NewLife;
+using NewLife.Log;
+using NewLife.Reflection;
+using NewLife.Remoting;
+using NewLife.Threading;
+
+namespace Zero.Desktop;
+
+public partial class FrmMain : Form
+{
+    public FrmMain()
+    {
+        InitializeComponent();
+    }
+
+    private void FrmMain_Load(Object sender, EventArgs e)
+    {
+        var asm = AssemblyX.Create(Assembly.GetExecutingAssembly());
+        Text = String.Format("{2} v{0} {1:HH:mm:ss}", asm.FileVersion, asm.Compile, Text);
+
+        richTextBox1.UseWinFormControl();
+
+        _timer = new TimerX(OnBindConn, null, 1_000, 3_000);
+    }
+
+    private TimerX _timer;
+    private ApiClient _client;
+    private String _lastConns;
+    private void OnBindConn(Object state)
+    {
+        //var keys = DAL.ConnStrs.Keys;
+        //var ks = keys.Join(",");
+        //if (ks == _lastConns) return;
+        //_lastConns = ks;
+
+        //cbConns.DataSource = keys;
+    }
+
+    private void btnOpen_Click(Object sender, EventArgs e)
+    {
+        var server = txtServer.Text;
+        if (server.IsNullOrEmpty()) return;
+
+        var btn = sender as Button;
+        var btn2 = btnOpenAsync;
+        if (btn.Text == "打开")
+        {
+            var client = new ApiClient(server)
+            {
+                Log = XTrace.Log,
+                EncoderLog = XTrace.Log,
+                SocketLog = XTrace.Log
+            };
+            client.Open();
+
+            var rs = client.Invoke<String[]>("api/all", null);
+            cbApi.DataSource = rs;
+
+            txtServer.Enabled = false;
+            groupBox2.Enabled = true;
+            btn.Text = "关闭";
+            btn2.Text = "异步关闭";
+
+            _client = client;
+        }
+        else
+        {
+            _client.Close(btn.Text);
+
+            txtServer.Enabled = true;
+            groupBox2.Enabled = false;
+            btn.Text = "打开";
+            btn2.Text = "异步打开";
+        }
+    }
+
+    private async void btnAsyncOpen_Click(object sender, EventArgs e)
+    {
+        var server = txtServer.Text;
+        if (server.IsNullOrEmpty()) return;
+
+        var btn = btnOpen;
+        var btn2 = sender as Button;
+        if (btn2.Text == "异步打开")
+        {
+            var client = new ApiClient(server)
+            {
+                Log = XTrace.Log,
+                EncoderLog = XTrace.Log,
+                SocketLog = XTrace.Log
+            };
+            client.Open();
+
+            var rs = await client.InvokeAsync<String[]>("api/all", null);
+            cbApi.DataSource = rs;
+
+            txtServer.Enabled = false;
+            groupBox2.Enabled = true;
+            btn.Text = "关闭";
+            btn2.Text = "异步关闭";
+
+            _client = client;
+        }
+        else
+        {
+            _client.Close(btn.Text);
+
+            txtServer.Enabled = true;
+            groupBox2.Enabled = false;
+            btn.Text = "打开";
+            btn2.Text = "异步打开";
+        }
+    }
+
+    private void listBox1_SelectedIndexChanged(Object sender, EventArgs e)
+    {
+        //var table = listBox1.SelectedItem as IDataTable;
+        //if (table == null) return;
+
+        //var sql = $"select * from {table.TableName}";
+        //var ds = _dal.Select(new SelectBuilder(sql), 0, 1000);
+
+        //dataGridView1.DataSource = ds.Tables[0];
+        //dataGridView1.Refresh();
+    }
+
+    private void btnCall_Click(object sender, EventArgs e)
+    {
+        var act = cbApi.Text.Substring(" ", "(");
+        var rs = _client.Invoke<String>(act, null);
+    }
+
+    private async void btnCallAsync_Click(object sender, EventArgs e)
+    {
+        var act = cbApi.Text.Substring(" ", "(");
+        var rs = await _client.InvokeAsync<String>(act, null);
+    }
+}
\ No newline at end of file
Added +238 -0
diff --git a/ZeroClient/FrmMain.Designer.cs b/ZeroClient/FrmMain.Designer.cs
new file mode 100644
index 0000000..cfda26e
--- /dev/null
+++ b/ZeroClient/FrmMain.Designer.cs
@@ -0,0 +1,238 @@
+namespace Zero.Desktop
+{
+    partial class FrmMain
+    {
+        /// <summary>
+        ///  Required designer variable.
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        ///  Clean up any resources being used.
+        /// </summary>
+        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows Form Designer generated code
+
+        /// <summary>
+        ///  Required method for Designer support - do not modify
+        ///  the contents of this method with the code editor.
+        /// </summary>
+        private void InitializeComponent()
+        {
+            groupBox1 = new GroupBox();
+            btnOpenAsync = new Button();
+            txtServer = new TextBox();
+            btnOpen = new Button();
+            label1 = new Label();
+            groupBox2 = new GroupBox();
+            textBox2 = new TextBox();
+            label3 = new Label();
+            btnCall = new Button();
+            cbApi = new ComboBox();
+            label2 = new Label();
+            groupBox3 = new GroupBox();
+            richTextBox1 = new RichTextBox();
+            btnCallAsync = new Button();
+            groupBox1.SuspendLayout();
+            groupBox2.SuspendLayout();
+            groupBox3.SuspendLayout();
+            SuspendLayout();
+            // 
+            // groupBox1
+            // 
+            groupBox1.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+            groupBox1.Controls.Add(btnOpenAsync);
+            groupBox1.Controls.Add(txtServer);
+            groupBox1.Controls.Add(btnOpen);
+            groupBox1.Controls.Add(label1);
+            groupBox1.Location = new Point(11, 10);
+            groupBox1.Margin = new Padding(3, 2, 3, 2);
+            groupBox1.Name = "groupBox1";
+            groupBox1.Padding = new Padding(3, 2, 3, 2);
+            groupBox1.Size = new Size(1134, 75);
+            groupBox1.TabIndex = 0;
+            groupBox1.TabStop = false;
+            groupBox1.Text = "数据库连接";
+            // 
+            // btnOpenAsync
+            // 
+            btnOpenAsync.Location = new Point(630, 19);
+            btnOpenAsync.Margin = new Padding(3, 2, 3, 2);
+            btnOpenAsync.Name = "btnOpenAsync";
+            btnOpenAsync.Size = new Size(106, 45);
+            btnOpenAsync.TabIndex = 4;
+            btnOpenAsync.Text = "异步打开";
+            btnOpenAsync.UseVisualStyleBackColor = true;
+            btnOpenAsync.Click += btnAsyncOpen_Click;
+            // 
+            // txtServer
+            // 
+            txtServer.Location = new Point(96, 28);
+            txtServer.Name = "txtServer";
+            txtServer.Size = new Size(346, 26);
+            txtServer.TabIndex = 3;
+            txtServer.Text = "tcp://127.0.0.1:5500";
+            // 
+            // btnOpen
+            // 
+            btnOpen.Location = new Point(489, 19);
+            btnOpen.Margin = new Padding(3, 2, 3, 2);
+            btnOpen.Name = "btnOpen";
+            btnOpen.Size = new Size(106, 45);
+            btnOpen.TabIndex = 2;
+            btnOpen.Text = "打开";
+            btnOpen.UseVisualStyleBackColor = true;
+            btnOpen.Click += btnOpen_Click;
+            // 
+            // label1
+            // 
+            label1.AutoSize = true;
+            label1.Location = new Point(19, 31);
+            label1.Name = "label1";
+            label1.Size = new Size(60, 20);
+            label1.TabIndex = 1;
+            label1.Text = "连接:";
+            // 
+            // groupBox2
+            // 
+            groupBox2.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+            groupBox2.Controls.Add(btnCallAsync);
+            groupBox2.Controls.Add(textBox2);
+            groupBox2.Controls.Add(label3);
+            groupBox2.Controls.Add(btnCall);
+            groupBox2.Controls.Add(cbApi);
+            groupBox2.Controls.Add(label2);
+            groupBox2.Enabled = false;
+            groupBox2.Location = new Point(11, 90);
+            groupBox2.Margin = new Padding(3, 2, 3, 2);
+            groupBox2.Name = "groupBox2";
+            groupBox2.Padding = new Padding(3, 2, 3, 2);
+            groupBox2.Size = new Size(1134, 202);
+            groupBox2.TabIndex = 1;
+            groupBox2.TabStop = false;
+            groupBox2.Text = "内容区";
+            // 
+            // textBox2
+            // 
+            textBox2.Location = new Point(96, 79);
+            textBox2.Name = "textBox2";
+            textBox2.Size = new Size(346, 26);
+            textBox2.TabIndex = 4;
+            // 
+            // label3
+            // 
+            label3.AutoSize = true;
+            label3.Location = new Point(19, 82);
+            label3.Name = "label3";
+            label3.Size = new Size(69, 20);
+            label3.TabIndex = 3;
+            label3.Text = "参数1:";
+            // 
+            // btnCall
+            // 
+            btnCall.Location = new Point(489, 67);
+            btnCall.Name = "btnCall";
+            btnCall.Size = new Size(106, 45);
+            btnCall.TabIndex = 2;
+            btnCall.Text = "调用";
+            btnCall.UseVisualStyleBackColor = true;
+            btnCall.Click += btnCall_Click;
+            // 
+            // cbApi
+            // 
+            cbApi.FormattingEnabled = true;
+            cbApi.Location = new Point(96, 35);
+            cbApi.Name = "cbApi";
+            cbApi.Size = new Size(346, 28);
+            cbApi.TabIndex = 1;
+            // 
+            // label2
+            // 
+            label2.AutoSize = true;
+            label2.Location = new Point(19, 38);
+            label2.Name = "label2";
+            label2.Size = new Size(60, 20);
+            label2.TabIndex = 0;
+            label2.Text = "接口:";
+            // 
+            // groupBox3
+            // 
+            groupBox3.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+            groupBox3.Controls.Add(richTextBox1);
+            groupBox3.Location = new Point(14, 296);
+            groupBox3.Margin = new Padding(3, 2, 3, 2);
+            groupBox3.Name = "groupBox3";
+            groupBox3.Padding = new Padding(3, 2, 3, 2);
+            groupBox3.Size = new Size(1128, 400);
+            groupBox3.TabIndex = 2;
+            groupBox3.TabStop = false;
+            groupBox3.Text = "日志";
+            // 
+            // richTextBox1
+            // 
+            richTextBox1.Dock = DockStyle.Fill;
+            richTextBox1.Location = new Point(3, 21);
+            richTextBox1.Margin = new Padding(3, 2, 3, 2);
+            richTextBox1.Name = "richTextBox1";
+            richTextBox1.Size = new Size(1122, 377);
+            richTextBox1.TabIndex = 0;
+            richTextBox1.Text = "";
+            // 
+            // btnCallAsync
+            // 
+            btnCallAsync.Location = new Point(630, 67);
+            btnCallAsync.Name = "btnCallAsync";
+            btnCallAsync.Size = new Size(106, 45);
+            btnCallAsync.TabIndex = 5;
+            btnCallAsync.Text = "异步调用";
+            btnCallAsync.UseVisualStyleBackColor = true;
+            btnCallAsync.Click += btnCallAsync_Click;
+            // 
+            // FrmMain
+            // 
+            AutoScaleDimensions = new SizeF(10F, 20F);
+            AutoScaleMode = AutoScaleMode.Font;
+            ClientSize = new Size(1155, 707);
+            Controls.Add(groupBox3);
+            Controls.Add(groupBox2);
+            Controls.Add(groupBox1);
+            Margin = new Padding(3, 2, 3, 2);
+            Name = "FrmMain";
+            StartPosition = FormStartPosition.CenterScreen;
+            Text = "零代客户端";
+            Load += FrmMain_Load;
+            groupBox1.ResumeLayout(false);
+            groupBox1.PerformLayout();
+            groupBox2.ResumeLayout(false);
+            groupBox2.PerformLayout();
+            groupBox3.ResumeLayout(false);
+            ResumeLayout(false);
+        }
+
+        #endregion
+
+        private GroupBox groupBox1;
+        private GroupBox groupBox2;
+        private Label label1;
+        private Button btnOpen;
+        private GroupBox groupBox3;
+        private RichTextBox richTextBox1;
+        private TextBox txtServer;
+        private Button btnCall;
+        private ComboBox cbApi;
+        private Label label2;
+        private TextBox textBox2;
+        private Label label3;
+        private Button btnOpenAsync;
+        private Button btnCallAsync;
+    }
+}
\ No newline at end of file
Added +120 -0
diff --git a/ZeroClient/FrmMain.resx b/ZeroClient/FrmMain.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/ZeroClient/FrmMain.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!--
+    Microsoft ResX Schema
+
+    Version 2.0
+
+    The primary goals of this format is to allow a simple XML format
+    that is mostly human readable. The generation and parsing of the
+    various data types are done through the TypeConverter classes
+    associated with the data types.
+
+    Example:
+
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+
+    There are any number of "resheader" rows that contain simple
+    name/value pairs.
+
+    Each data row contains a name, and value. The row also contains a
+    type or mimetype. Type corresponds to a .NET class that support
+    text/value conversion through the TypeConverter architecture.
+    Classes that don't support this are serialized and stored with the
+    mimetype set.
+
+    The mimetype is used for serialized objects, and tells the
+    ResXResourceReader how to depersist the object. This is currently not
+    extensible. For a given mimetype the value must be set accordingly:
+
+    Note - application/x-microsoft.net.object.binary.base64 is the format
+    that the ResXResourceWriter will generate, however the reader can
+    read any of the formats listed below.
+
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>
\ No newline at end of file
Added +73 -0
diff --git a/ZeroClient/Program.cs b/ZeroClient/Program.cs
new file mode 100644
index 0000000..14dd759
--- /dev/null
+++ b/ZeroClient/Program.cs
@@ -0,0 +1,73 @@
+using System.Text;
+using NewLife;
+using NewLife.Log;
+using NewLife.Model;
+using Stardust;
+
+namespace Zero.Desktop;
+
+internal static class Program
+{
+    /// <summary>
+    ///  The main entry point for the application.
+    /// </summary>
+    [STAThread]
+    static void Main()
+    {
+        // 支持GB2312编码
+        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+        XTrace.UseWinForm();
+        MachineInfo.RegisterAsync();
+
+        StartClient();
+
+        var set = ClientSetting.Current;
+
+        // 启用语音提示
+        StringHelper.EnableSpeechTip = set.SpeechTip;
+
+        if (set.IsNew) "学无先后达者为师,欢迎使用新生命零代客户端!".SpeechTip();
+
+        // To customize application configuration such as set high DPI settings or default font,
+        // see https://aka.ms/applicationconfiguration.
+        ApplicationConfiguration.Initialize();
+        //Application.EnableVisualStyles();
+        //Application.SetCompatibleTextRenderingDefault(false);
+        //Application.SetHighDpiMode(HighDpiMode.SystemAware);
+        Application.Run(new FrmMain());
+    }
+
+    static StarFactory _factory;
+    static StarClient _Client;
+    private static void StartClient()
+    {
+        var set = ClientSetting.Current;
+        var server = set.Server;
+        if (server.IsNullOrEmpty()) return;
+
+        XTrace.WriteLine("初始化服务端地址:{0}", server);
+
+        _factory = new StarFactory(server, null, null)
+        {
+            Log = XTrace.Log,
+        };
+
+        var client = new StarClient(server)
+        {
+            Code = set.Code,
+            Secret = set.Secret,
+            ProductCode = _factory.AppId,
+            Setting = set,
+
+            Tracer = _factory.Tracer,
+            Log = XTrace.Log,
+        };
+
+        client.Open();
+
+        Host.RegisterExit(() => client.Logout("ApplicationExit"));
+
+        _Client = client;
+    }
+}
\ No newline at end of file
Added +37 -0
diff --git a/ZeroClient/Zero.Desktop.csproj b/ZeroClient/Zero.Desktop.csproj
new file mode 100644
index 0000000..5eae30c
--- /dev/null
+++ b/ZeroClient/Zero.Desktop.csproj
@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net9.0-windows</TargetFramework>
+    <AssemblyTitle>客户端桌面应用</AssemblyTitle>
+    <Description>CS架构的客户端桌面应用,给用户提供便捷操作,可对接硬件</Description>
+    <Company>新生命开发团队</Company>
+    <Copyright>©2002-2024 NewLife</Copyright>
+    <VersionPrefix>1.0</VersionPrefix>
+    <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+    <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+    <FileVersion>$(Version)</FileVersion>
+    <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
+    <Deterministic>false</Deterministic>
+    <OutputPath>..\Bin\Desktop</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <LangVersion>latest</LangVersion>
+    <UseWindowsForms>true</UseWindowsForms>
+
+    <ApplicationVisualStyles>true</ApplicationVisualStyles>
+    <ApplicationUseCompatibleTextRendering>false</ApplicationUseCompatibleTextRendering>
+    <ApplicationHighDpiMode>SystemAware</ApplicationHighDpiMode>
+    <ApplicationDefaultFont>Microsoft Sans Serif, 8.25pt</ApplicationDefaultFont>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.Stardust" Version="3.1.2024.1004" />
+    <PackageReference Include="System.Speech" Version="8.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\NewLife.Remoting\NewLife.Remoting.csproj" />
+  </ItemGroup>
+
+</Project>
\ No newline at end of file