Compare commits

...
2 Commits
Author SHA1 Message Date
jimmy 479eae64a1 A:PLC 调试页面增加串口设置 2026-01-21 14:42:45 +08:00
jimmy 4973979169 A:补充部分缺失功能
U:调整元信息数据
2026-01-21 13:55:53 +08:00
20 changed files with 458 additions and 94 deletions

No files matched your search

+4 -4
View File
@@ -2,17 +2,17 @@
## Project Structure & Module Organization
- Solution: `Sharp.sln` aggregates all modules in this folder.
- Libraries: `Common`, `Config`, `Log`, `EventBus`, `Sharp.Infrastructure`.
- Libraries: `Common`, `Config`, `Log`, `Cache`, `EventBus`, `Data`, `NetServer`, `NetClient`, `Ports`, `Plc`, `Plc.Siemens`, `Process`, `Python`, `Wpf`, `Wpf.HandyControl`, `Wpf.DevTools`.
- Networking: `NetServer` (HTTP server + middleware), `NetClient` (typed HTTP client).
- UI: `Wpf` (shared WPF helpers), `Client` (WPF app using `Wpf`).
- UI: `Wpf` (shared WPF helpers), `Wpf.HandyControl`, `Wpf.DevTools`, `Sharp.Demo` (WPF demo app).
- Examples: `NetServer/Examples` contains a runnable sample entry point.
- Tests: solution includes external projects under `../../test/*` (e.g., `Common.Test`, `NetClient.Test`).
## Build, Test, and Development Commands
- Restore: `dotnet restore`
- Build (Debug/Release): `dotnet build Sharp.sln -c Debug` | `-c Release`
- Run server example: `dotnet run --project NetServer/NetServer.csproj`
- Run WPF client (Windows): `dotnet run --project Client/Client.csproj`
- Run server example: `dotnet run --project NetServer/Examples/NetServer.Examples.csproj`
- Run WPF demo (Windows): `dotnet run --project Sharp.Demo/Sharp.Demo.csproj`
- Run all tests: `dotnet test Sharp.sln -c Debug --no-build`
- Run a single test project: `dotnet test ../../test/Common.Test/Common.Test.csproj`
+1 -1
View File
@@ -8,7 +8,7 @@
<RootNamespace>Sharp.Cache</RootNamespace>
<!-- 程序集元数据 -->
<Description>Sharp Framework 缓存库 - 内存缓存抽象和实现(规划中)</Description>
<Description>Sharp Framework 缓存库 - 内存缓存抽象和实现</Description>
<!-- NuGet 包元数据 -->
<PackageTags>sharp;cache;memory-cache;caching</PackageTags>
+3 -7
View File
@@ -1,7 +1,5 @@
# Sharp.Cache
> **🚧 开发中 / Under Construction**
轻量级内存缓存模块,基于 `Microsoft.Extensions.Caching.Memory`,提供简洁的缓存API。
## 📋 为什么需要 Cache?
@@ -72,7 +70,7 @@ public class ProductService {
---
## 🚀 快速开始(未来)
## 🚀 快速开始
### 1. 注册服务
@@ -646,7 +644,5 @@ public async Task<Result<Product>> GetProductAsync(int id) {
---
**状态**:设计阶段,未实现
**预计实现时间**:根据实际需求决定
**实现难度**:低(100行代码即可完成核心功能)
**依赖**:Microsoft.Extensions.Caching.Memory(.NET内置)
**状态**:已实现(内存缓存)
**当前实现**:IMemoryCache 封装 + 绝对/滑动过期 + 前缀移除
+22
View File
@@ -0,0 +1,22 @@
<Project>
<PropertyGroup>
<Authors Condition="'$(Authors)' == ''">duyl328</Authors>
<PackageProjectUrl Condition="'$(PackageProjectUrl)' == ''">https://seyl.me</PackageProjectUrl>
<PackageLicenseFile Condition="'$(PackageLicenseFile)' == ''">LICENSE</PackageLicenseFile>
<PackageReadmeFile Condition="'$(PackageReadmeFile)' == ''">README.md</PackageReadmeFile>
<PackageRequireLicenseAcceptance Condition="'$(PackageRequireLicenseAcceptance)' == ''">false</PackageRequireLicenseAcceptance>
<Version Condition="'$(Version)' == ''">1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)../../LICENSE"
Pack="true"
PackagePath=""
Link="LICENSE"
Condition="Exists('$(MSBuildThisFileDirectory)../../LICENSE')" />
<None Update="README.md"
Pack="true"
PackagePath=""
Condition="Exists('$(MSBuildProjectDirectory)/README.md')" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -8,7 +8,7 @@
<RootNamespace>Sharp.EventBus</RootNamespace>
<!-- 程序集元数据 -->
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅(规划中)</Description>
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅</Description>
<!-- NuGet 包元数据 -->
<PackageTags>sharp;eventbus;pub-sub;events</PackageTags>
+3 -6
View File
@@ -1,7 +1,5 @@
# Sharp.EventBus
> **🚧 开发中 / Under Construction**
轻量级进程内事件总线,用于解耦应用程序中的模块通信。
## 📋 为什么需要 EventBus?
@@ -74,7 +72,7 @@ public class PlcDataLogger {
---
## 🚀 快速开始(未来)
## 🚀 快速开始
### 1. 注册服务
@@ -472,6 +470,5 @@ System.Threading.Channels.Channel<EventEnvelope>
---
**状态**:设计阶段,未实现
**预计实现时间**:根据实际需求决定
**当前替代方案**:使用 Common 模块的 Global 容器 + 委托事件
**状态**:已实现(进程内 InMemoryEventBus)
**当前实现**:基于 Channel 的异步队列 + 订阅优先级/过滤
+7 -7
View File
@@ -1,7 +1,7 @@
# Sharp Project Context
## Project Overview
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF-based client/server applications. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF UI libraries with a demo application. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
## Key Modules
@@ -20,9 +20,10 @@
- **Ports**: Serial/COM port abstractions.
### Applications (UI)
- **Client**: Main WPF Client application.
- **Server**: WPF Server application (likely a dashboard or local control interface).
- **Sharp.Demo**: WPF demo application showcasing key modules.
- **Wpf**: Shared WPF UI logic, behaviors, and converters.
- **Wpf.HandyControl**: HandyControl integration and dialog services.
- **Wpf.DevTools**: WPF developer tooling (visual tree/log/perf).
## Development Workflow
@@ -39,14 +40,13 @@
```bash
dotnet build Sharp.sln -c Debug
```
- **Run WPF Client**:
- **Run WPF Demo**:
```bash
dotnet run --project Client/Client.csproj
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
```
- **Run NetServer Example**:
```bash
dotnet run --project NetServer/Examples
# Note: Check specific project path if Examples is a folder or project
dotnet run --project NetServer/Examples/NetServer.Examples.csproj
```
### Testing
+16 -1
View File
@@ -5,6 +5,7 @@ using Serilog;
using Serilog.Events;
using Serilog.Formatting.Json;
using Sharp.Log.Options;
using System.Text;
namespace Sharp.Log.Internal;
@@ -47,6 +48,7 @@ internal static class SerilogConfigurator
// Sinks
if (options.UseConsole)
{
TrySetConsoleEncoding();
if (options.ConsoleJson)
{
cfg = cfg.WriteTo.Async(a => a.Console(new JsonFormatter(renderMessage: true)));
@@ -94,6 +96,19 @@ internal static class SerilogConfigurator
return cfg.CreateLogger();
}
private static void TrySetConsoleEncoding()
{
try
{
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;
}
catch
{
// Ignore environments without a console.
}
}
private static LogEventLevel MapLevel(LogLevel level)
{
return level switch
@@ -133,4 +148,4 @@ internal static class SerilogConfigurator
return null;
}
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Sharp.NetServer.Examples</AssemblyName>
<RootNamespace>Sharp.NetServer.Examples</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NetServer.csproj" />
</ItemGroup>
</Project>
+4
View File
@@ -18,6 +18,10 @@
<ProjectReference Include="..\Config\Config.csproj"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="Examples/**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0"/>
+14 -14
View File
@@ -10,11 +10,11 @@
- **[Sharp.Common](Common/README.md)**: 基础工具库。提供轻量级 DI 容器 (`Global`)、函数式结果模式 (`Result<T>`)、以及丰富的扩展方法(String, DateTime, Encryption, Reflection)。
- **[Sharp.Config](Config/README.md)**: 强大的配置管理。支持 JSON/XML/INI/TOML 多源聚合、强类型绑定和数据注解验证。
- **[Sharp.Log](Log/README.md)**: 统一日志门面。基于 Serilog 封装,支持一行代码接入,提供结构化日志和文件滚动策略。
- **[Sharp.EventBus](EventBus/README.md)**: (开发中) 轻量级事件总线。
- **[Sharp.EventBus](EventBus/README.md)**: 轻量级事件总线。
- **[Sharp.Cache](Cache/README.md)**: 轻量级内存缓存抽象与实现。
### 💾 数据与存储
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供开箱即用的仓储模式、自动审计(创建人/时间)、软删除和分页支持。
- **[Sharp.Repository](Repository/README.md)**: 数据库集成模块。通过配置切换 SQL Server/MySQL/PostgreSQL/Sqlite,支持自动迁移。
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供仓储模式、自动审计(创建人/时间)、软删除和分页支持,兼容多种数据库。
### 🌐 网络与通信
- **[Sharp.NetServer](NetServer/README.md)**: 轻量级 Web 框架。基于 `HttpListener`,提供类似 ASP.NET Core 的 Controller/Route/Middleware 开发体验,适合嵌入式或桌面应用内嵌 Web 服务。
@@ -27,8 +27,13 @@
### 🖥️ 桌面应用 UI
- **[Sharp.Wpf](Wpf/README.md)**: WPF 基础库。提供 MVVM 基类、全局异常捕获、常用转换器、对话框服务和行为扩展。
- **[Sharp.Client](Client/README.md)**: 综合客户端示例。集成 PLC 监控、串口调试和用户登录功能的工业控制台应用。
- **[Sharp.Server](Server/README.md)**: 服务器控制台示例。展示了如何在一个 WPF 应用中托管 NetServer Web 服务,并提供可视化管理界面。
- **[Sharp.Wpf.HandyControl](Wpf.HandyControl/README.md)**: HandyControl 组件库集成与对话框服务。
- **[Sharp.Wpf.DevTools](Wpf.DevTools/README.md)**: WPF 开发者工具库(可视化树、日志、性能监控)。
- **[Sharp.Demo](Sharp.Demo/README.md)**: 综合演示应用,覆盖主要模块用法。
### 🧩 工具与集成
- **[Sharp.Process](Process/README.md)**: 外部进程执行、管理和监控。
- **[Sharp.Python](Python/README.md)**: Python 脚本执行与互操作。
## 🚀 快速开始
@@ -47,19 +52,14 @@ dotnet build Sharp.sln -c Debug
### 运行示例
**1. 运行 WPF 客户端:**
**1. 运行 WPF 综合演示:**
```bash
dotnet run --project Client/Client.csproj
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
```
**2. 运行嵌入式 Web 服务器:**
**2. 运行 NetServer 纯控制台示例:**
```bash
dotnet run --project Server/Server.csproj
```
**3. 运行 NetServer 纯控制台示例:**
```bash
dotnet run --project NetServer/Examples
dotnet run --project NetServer/Examples/NetServer.Examples.csproj
```
## 🧪 测试
+1 -6
View File
@@ -8,20 +8,15 @@
<UseWPF>true</UseWPF>
<AssemblyName>Sharp.Demo</AssemblyName>
<RootNamespace>Sharp.Demo</RootNamespace>
<IsPackable>false</IsPackable>
<!-- 程序集元数据 -->
<Description>Sharp Framework 综合演示应用 - 展示所有模块的最佳实践和使用方式</Description>
<!-- NuGet 包元数据 -->
<PackageTags>sharp;demo;wpf;framework;example;best-practices</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<!-- 包含 README 到 NuGet 包 -->
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
<!-- NuGet 包 -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
@@ -9,6 +9,7 @@ using Sharp.Wpf.Navigation;
using Sharp.Wpf.Abstractions;
using Plc.Abstractions;
using Plc.Models;
using Plc.Siemens.Options;
namespace Sharp.Demo.ViewModels.Pages;
@@ -20,8 +21,13 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
private readonly IPlcService _plcService;
private readonly ILogger<PlcDebugViewModel> _logger;
private readonly IDispatcher _dispatcher;
private readonly SiemensPlcOptions _siemensOptions;
private string _ipAddress = "192.168.1.100";
private PlcBrand _selectedBrand = PlcBrand.Siemens;
private string _siemensIpAddress;
private string _siemensPort;
private string _omronIpAddress = "192.168.1.100";
private string _omronPort = "9600";
private string _status = "未连接";
private string _readAddress = "DB1.DBW0";
private string _readValue = "-";
@@ -36,11 +42,16 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
public PlcDebugViewModel(
IPlcService plcService,
ILogger<PlcDebugViewModel> logger,
IDispatcher dispatcher)
IDispatcher dispatcher,
SiemensPlcOptions siemensOptions)
{
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_siemensOptions = siemensOptions ?? throw new ArgumentNullException(nameof(siemensOptions));
_siemensIpAddress = _siemensOptions.IpAddress;
_siemensPort = _siemensOptions.Port.ToString();
// 初始化命令
ConnectCommand = new AsyncCommand(ConnectAsync);
@@ -63,10 +74,62 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
#region 属性
public PlcBrand SelectedBrand
{
get => _selectedBrand;
set
{
if (SetProperty(ref _selectedBrand, value))
{
OnPropertyChanged(nameof(IpAddress));
OnPropertyChanged(nameof(Port));
OnPropertyChanged(nameof(IsBrandSupported));
OnPropertyChanged(nameof(SelectedBrandName));
OnPropertyChanged(nameof(SelectedBrandDescription));
}
}
}
public string SelectedBrandName => SelectedBrand switch
{
PlcBrand.Siemens => "西门子",
PlcBrand.Omron => "欧姆龙",
PlcBrand.Mitsubishi => "三菱",
PlcBrand.ModbusTcp => "Modbus TCP",
_ => "未知"
};
public string SelectedBrandDescription => SelectedBrand switch
{
PlcBrand.Siemens => "Sharp.Plc.Siemens - 西门子 PLC 通信驱动 (S7-1200/1500/300/400)",
PlcBrand.Omron => "Sharp.Plc.Omron - 欧姆龙 PLC 通信驱动(规划中)",
_ => "Sharp.Plc - PLC 通信驱动"
};
public bool IsBrandSupported => SelectedBrand == PlcBrand.Siemens;
public string IpAddress
{
get => _ipAddress;
set => SetProperty(ref _ipAddress, value);
get => SelectedBrand == PlcBrand.Siemens ? _siemensIpAddress : _omronIpAddress;
set
{
if (SelectedBrand == PlcBrand.Siemens)
SetProperty(ref _siemensIpAddress, value, nameof(IpAddress));
else
SetProperty(ref _omronIpAddress, value, nameof(IpAddress));
}
}
public string Port
{
get => SelectedBrand == PlcBrand.Siemens ? _siemensPort : _omronPort;
set
{
if (SelectedBrand == PlcBrand.Siemens)
SetProperty(ref _siemensPort, value, nameof(Port));
else
SetProperty(ref _omronPort, value, nameof(Port));
}
}
public string Status
@@ -198,8 +261,21 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
{
try
{
if (!IsBrandSupported)
{
Status = "当前品牌暂未支持";
AppendLog($"⚠️ {SelectedBrandName} 驱动尚未实现");
return;
}
if (!TryApplySiemensOptions())
{
Status = "配置错误";
return;
}
Status = "正在连接...";
AppendLog($"正在连接 PLC: {IpAddress}");
AppendLog($"正在连接 PLC ({SelectedBrandName}): {IpAddress}:{Port}");
await _plcService.StartAsync();
@@ -318,6 +394,25 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
}
}
private bool TryApplySiemensOptions()
{
if (string.IsNullOrWhiteSpace(IpAddress))
{
AppendLog("❌ IP 地址不能为空");
return false;
}
if (!int.TryParse(Port, out var port) || port < 1 || port > 65535)
{
AppendLog("❌ 端口格式不正确(1-65535)");
return false;
}
_siemensOptions.IpAddress = IpAddress;
_siemensOptions.Port = port;
return true;
}
private void ClearLog()
{
LogText = string.Empty;
@@ -637,4 +732,4 @@ public class WriteQueueItem : ViewModelBase
get => _dataTypeName;
set => SetProperty(ref _dataTypeName, value);
}
}
}
@@ -11,6 +11,7 @@ using Sharp.Wpf.Commands;
using Sharp.Wpf.Abstractions;
using Plc.Abstractions;
using Plc.Models;
using Plc.Siemens.Options;
namespace Sharp.Demo.ViewModels.Windows;
@@ -22,8 +23,13 @@ public class PlcDebugViewModel : ViewModelBase
private readonly IPlcService _plcService;
private readonly ILogger<PlcDebugViewModel> _logger;
private readonly IDispatcher _dispatcher;
private readonly SiemensPlcOptions _siemensOptions;
private string _ipAddress = "192.168.1.100";
private PlcBrand _selectedBrand = PlcBrand.Siemens;
private string _siemensIpAddress;
private string _siemensPort;
private string _omronIpAddress = "192.168.1.100";
private string _omronPort = "9600";
private string _status = "未连接";
private string _readAddress = "DB1.DBW0";
private string _readValue = "-";
@@ -38,11 +44,16 @@ public class PlcDebugViewModel : ViewModelBase
public PlcDebugViewModel(
IPlcService plcService,
ILogger<PlcDebugViewModel> logger,
IDispatcher dispatcher)
IDispatcher dispatcher,
SiemensPlcOptions siemensOptions)
{
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_siemensOptions = siemensOptions ?? throw new ArgumentNullException(nameof(siemensOptions));
_siemensIpAddress = _siemensOptions.IpAddress;
_siemensPort = _siemensOptions.Port.ToString();
// 初始化命令
ConnectCommand = new AsyncCommand(ConnectAsync);
@@ -66,10 +77,54 @@ public class PlcDebugViewModel : ViewModelBase
#region 属性
public PlcBrand SelectedBrand
{
get => _selectedBrand;
set
{
if (SetProperty(ref _selectedBrand, value))
{
OnPropertyChanged(nameof(IpAddress));
OnPropertyChanged(nameof(Port));
OnPropertyChanged(nameof(IsBrandSupported));
OnPropertyChanged(nameof(SelectedBrandName));
}
}
}
public string SelectedBrandName => SelectedBrand switch
{
PlcBrand.Siemens => "西门子",
PlcBrand.Omron => "欧姆龙",
PlcBrand.Mitsubishi => "三菱",
PlcBrand.ModbusTcp => "Modbus TCP",
_ => "未知"
};
public bool IsBrandSupported => SelectedBrand == PlcBrand.Siemens;
public string IpAddress
{
get => _ipAddress;
set => SetProperty(ref _ipAddress, value);
get => SelectedBrand == PlcBrand.Siemens ? _siemensIpAddress : _omronIpAddress;
set
{
if (SelectedBrand == PlcBrand.Siemens)
SetProperty(ref _siemensIpAddress, value, nameof(IpAddress));
else
SetProperty(ref _omronIpAddress, value, nameof(IpAddress));
}
}
public string Port
{
get => SelectedBrand == PlcBrand.Siemens ? _siemensPort : _omronPort;
set
{
if (SelectedBrand == PlcBrand.Siemens)
SetProperty(ref _siemensPort, value, nameof(Port));
else
SetProperty(ref _omronPort, value, nameof(Port));
}
}
public string Status
@@ -187,8 +242,21 @@ public class PlcDebugViewModel : ViewModelBase
{
try
{
if (!IsBrandSupported)
{
Status = "当前品牌暂未支持";
AppendLog($"⚠️ {SelectedBrandName} 驱动尚未实现");
return;
}
if (!TryApplySiemensOptions())
{
Status = "配置错误";
return;
}
Status = "正在连接...";
AppendLog($"正在连接 PLC: {IpAddress}");
AppendLog($"正在连接 PLC ({SelectedBrandName}): {IpAddress}:{Port}");
await _plcService.StartAsync();
@@ -307,6 +375,25 @@ public class PlcDebugViewModel : ViewModelBase
}
}
private bool TryApplySiemensOptions()
{
if (string.IsNullOrWhiteSpace(IpAddress))
{
AppendLog("❌ IP 地址不能为空");
return false;
}
if (!int.TryParse(Port, out var port) || port < 1 || port > 65535)
{
AppendLog("❌ 端口格式不正确(1-65535)");
return false;
}
_siemensOptions.IpAddress = IpAddress;
_siemensOptions.Port = port;
return true;
}
private void AppendLog(string message)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
@@ -864,4 +951,4 @@ public class WriteQueueItem : ViewModelBase
get => _verify;
set => SetProperty(ref _verify, value);
}
}
}
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:plc="clr-namespace:Plc.Models;assembly=Plc"
mc:Ignorable="d"
d:DesignHeight="1000"
d:DesignWidth="1200">
@@ -18,7 +19,7 @@
Margin="0,10,0,30"
FontSize="14"
Foreground="{DynamicResource SecondaryTextBrush}"
Text="Sharp.Plc.Siemens - 西门子 PLC 通信驱动 (S7-1200/1500/300/400)" />
Text="{Binding SelectedBrandDescription}" />
<!-- 连接控制 -->
<Border
@@ -31,11 +32,40 @@
<StackPanel>
<TextBlock Text="PLC 连接" FontSize="16" FontWeight="Bold" Margin="0,0,0,10" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" Width="80" />
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
</StackPanel>
<TabControl SelectedValue="{Binding SelectedBrand}" SelectedValuePath="Tag" Margin="0,0,0,10">
<TabItem Header="西门子" Tag="{x:Static plc:PlcBrand.Siemens}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"
Width="80" />
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="15,0,10,0" Width="50" />
<TextBox Text="{Binding Port}" Width="100" Height="32" Padding="8,0"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
</StackPanel>
<TextBlock Text="S7-1200/1500/300/400,默认端口 102"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12" />
</StackPanel>
</TabItem>
<TabItem Header="欧姆龙" Tag="{x:Static plc:PlcBrand.Omron}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"
Width="80" />
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="15,0,10,0" Width="50" />
<TextBox Text="{Binding Port}" Width="100" Height="32" Padding="8,0"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
</StackPanel>
<TextBlock Text="欧姆龙驱动尚未实现,后续可按品牌扩展配置项"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12" />
</StackPanel>
</TabItem>
</TabControl>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0" Width="80" />
@@ -45,10 +75,12 @@
<StackPanel Orientation="Horizontal">
<Button Content="连接" Width="100" Height="35" Margin="0,0,10,0"
Command="{Binding ConnectCommand}"
Style="{StaticResource ButtonSuccess}" />
Style="{StaticResource ButtonSuccess}"
IsEnabled="{Binding IsBrandSupported}" />
<Button Content="断开" Width="100" Height="35"
Command="{Binding DisconnectCommand}"
Style="{StaticResource ButtonDanger}" />
Style="{StaticResource ButtonDanger}"
IsEnabled="{Binding IsBrandSupported}" />
</StackPanel>
</StackPanel>
</Border>
@@ -296,4 +328,4 @@
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
</UserControl>
@@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:plc="clr-namespace:Plc.Models;assembly=Plc"
mc:Ignorable="d"
Title="PLC 调试工具"
Height="800"
@@ -23,16 +24,38 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
<TabControl Grid.Row="0" SelectedValue="{Binding SelectedBrand}" SelectedValuePath="Tag"
Margin="0,0,0,10">
<TabItem Header="西门子" Tag="{x:Static plc:PlcBrand.Siemens}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="10,0,10,0" />
<TextBox Text="{Binding Port}" Width="80" Height="28" Margin="0,0,10,0" />
</StackPanel>
</TabItem>
<TabItem Header="欧姆龙" Tag="{x:Static plc:PlcBrand.Omron}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="10,0,10,0" />
<TextBox Text="{Binding Port}" Width="80" Height="28" Margin="0,0,10,0" />
<TextBlock Text="驱动未实现" VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}" />
</StackPanel>
</TabItem>
</TabControl>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0"
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}" />
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}"
IsEnabled="{Binding IsBrandSupported}" />
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0"
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}" />
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}"
IsEnabled="{Binding IsBrandSupported}" />
<Separator Margin="10,0" />
@@ -42,7 +65,7 @@
</StackPanel>
<!-- 最新日志 -->
<TextBlock Grid.Row="1" Text="{Binding LastLogMessage}"
<TextBlock Grid.Row="2" Text="{Binding LastLogMessage}"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="11" Margin="0,5,0,0" TextTrimming="CharacterEllipsis" />
</Grid>
@@ -318,4 +341,4 @@
</TabItem>
</TabControl>
</Grid>
</Window>
</Window>
@@ -0,0 +1,43 @@
<Window x:Class="Sharp.Wpf.HandyControl.Dialogs.InputDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="输入"
Width="420"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="MessageText"
TextWrapping="Wrap"
Margin="0,0,0,10" />
<TextBox x:Name="InputTextBox"
Grid.Row="1"
Height="32"
Padding="6,0"
MinWidth="300" />
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,15,0,0">
<Button x:Name="OkButton"
Content="确定"
Width="90"
Margin="0,0,10,0"
IsDefault="True"
Click="OkButton_Click" />
<Button x:Name="CancelButton"
Content="取消"
Width="90"
IsCancel="True"
Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,31 @@
using System.Windows;
namespace Sharp.Wpf.HandyControl.Dialogs;
public partial class InputDialog : Window
{
public string InputValue => InputTextBox.Text;
public InputDialog(string message, string? title = null, string? defaultValue = null)
{
InitializeComponent();
Title = string.IsNullOrWhiteSpace(title) ? "输入" : title;
MessageText.Text = message;
InputTextBox.Text = defaultValue ?? string.Empty;
Loaded += (_, _) =>
{
InputTextBox.Focus();
InputTextBox.SelectAll();
};
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
+4 -14
View File
@@ -304,7 +304,7 @@ public interface IHandyDialogService : IDialogService
Task<HandyDialogResult> AskAsync(string message, string? title = null);
/// <summary>
/// 显示输入对话框(尚未实现)
/// 显示输入对话框
/// </summary>
Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null);
@@ -1037,19 +1037,9 @@ services.AddSharpHandyControl();
services.AddSingleton<IDialogService, MyCustomDialogService>();
```
### Q5: `ShowInputAsync` 为什么返回失败?
### Q5: `ShowInputAsync` 如何返回结果?
**A:** `ShowInputAsync` 功能尚未实现(需要自定义对话框窗口),当前版本会返回:
```csharp
Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口")
```
如果需要输入功能,可以临时使用:
- 创建自定义 Window 进行输入
- 使用 HandyControl 的其他输入控件
- 等待后续版本实现
**A:** `ShowInputAsync` 会弹出输入对话框,点击“确定”返回 `Result.Success`,点击“取消”返回失败结果(错误信息为“用户取消输入”)。
### Q6: 能否在 WinForms 项目中使用?
@@ -1217,7 +1207,7 @@ protected override void OnStartup(StartupEventArgs e)
|------------------------------------------------|------------|---------------------------|--------|
| `ConfirmAsync(message, title)` | 确认对话框 | `Task<bool>` | 确定/取消 |
| `AskAsync(message, title)` | 询问对话框 | `Task<HandyDialogResult>` | 是/否/取消 |
| `ShowInputAsync(message, title, defaultValue)` | 输入对话框(未实现) | `Task<Result<string>>` | 确定/取消 |
| `ShowInputAsync(message, title, defaultValue)` | 输入对话框 | `Task<Result<string>>` | 确定/取消 |
### Growl 通知方法
@@ -2,6 +2,7 @@ using System.Windows;
using Common.Results;
using HandyControl.Controls;
using HandyControl.Data;
using Sharp.Wpf.HandyControl.Dialogs;
using Sharp.Wpf.HandyControl.Options;
using Sharp.Wpf.Services;
using MessageBox = HandyControl.Controls.MessageBox;
@@ -96,13 +97,29 @@ public class HandyDialogService : IHandyDialogService
}
/// <summary>
/// 显示输入对话框(尚未实现)
/// 显示输入对话框
/// </summary>
public Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null)
{
// TODO: 需要自定义 TextDialog 窗口来实现此功能
// 当前版本暂不支持,返回失败结果
return Task.FromResult(Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口"));
return Task.Run(() =>
{
if (Application.Current == null)
return Result.Failure<string>("当前应用上下文不可用");
return Application.Current.Dispatcher.Invoke(() =>
{
var dialog = new InputDialog(message, title, defaultValue)
{
Owner = Application.Current.MainWindow
};
var dialogResult = dialog.ShowDialog();
if (dialogResult == true)
return Result.Success(dialog.InputValue);
return Result.Failure<string>("用户取消输入");
});
});
}
/// <summary>
@@ -178,4 +195,4 @@ public class HandyDialogService : IHandyDialogService
}
#endregion
}
}