feat: 完成界面相关的代码开发
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Raman TUI: a Windows terminal application for controlling Raman spectrometers and filter wheels. The TUI accepts text commands (e.g. `spec scan`, `filter connect f1`) and also supports natural-language input via a DeepSeek LLM agent that translates prompts into those commands.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
# Build everything (solution file is roman.slnx, not .sln)
|
||||
dotnet build roman.slnx
|
||||
|
||||
# Run all tests (xUnit)
|
||||
dotnet test roman.slnx
|
||||
|
||||
# Run a single test project
|
||||
dotnet test test/Roman.Host.Tests
|
||||
dotnet test test/Raman.TUI.Tests
|
||||
|
||||
# Run a single test by filter
|
||||
dotnet test test/Roman.Host.Tests --filter "FullyQualifiedName~DiscoverFiltersUseCaseTests"
|
||||
|
||||
# Run the TUI in simulation mode (no hardware needed)
|
||||
dotnet run --project src/Raman.TUI -- --simulate
|
||||
```
|
||||
|
||||
The target framework is **net10.0**. The solution uses the `.slnx` format (XML-based solution file).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Layer structure (bottom to top)
|
||||
|
||||
**Raman.Core** — Hardware abstractions. Defines `ISpectrometerDevice`, `IFilterWheelDevice`, and their associated data types (settings, capabilities, spectra). No implementations here.
|
||||
|
||||
**Spec.Sea** — Spectrometer driver for the Sea SDK. Wraps a native C DLL (`SeaSDK.dll`) via `LibraryImport` P/Invoke in `SeaSdkNativeMethods`. `SeaSdkGateway` provides a managed facade; `SeaSpectrometerDevice`/`SeaSpectrometerDiscovery` implement the Core interfaces. Uses `AllowUnsafeBlocks`. The native DLL lives outside this repo at `../main/externals/SeaSDK.dll` and is copied to output.
|
||||
|
||||
**Filter.Ello** — Filter wheel driver for Elliptec serial-protocol devices. Communicates over COM ports (`System.IO.Ports`). `SerialProtocol` encodes/decodes the Elliptec hex-based command format. Supports 4-slot and 6-slot wheels.
|
||||
|
||||
**Raman.Simulator** — In-memory fakes of both spectrometers and filter wheels. Used for development and testing without hardware. Activated by `--simulate` flag or `RAMAN_TUI_SIMULATE=1` env var.
|
||||
|
||||
**Roman.Host** — Business logic orchestration layer. Contains:
|
||||
- `SpectrometerRuntimeWorkspace` / `FilterRuntimeWorkspace` — stateful command dispatchers that manage device lifecycle (discover, connect, configure, acquire, disconnect).
|
||||
- Request/response contracts (`SpectrometerRequest`/`SpectrometerResponse`, `FilterRequest`/`FilterResponse`) with command-kind enums.
|
||||
- Filter discovery subsystem that scans COM ports for Elliptec devices.
|
||||
- Filter configuration persistence (JSON-based `FilterConfigurationStore`).
|
||||
|
||||
**Raman.TUI** — The terminal application. Key components:
|
||||
- `ICommandSession` — interface for command handlers. `FilterCommandSession` and `SpectrometerCommandSession` parse text commands and delegate to Roman.Host workspaces. `CompositeCommandSession` routes to the right handler.
|
||||
- `InlineConsoleApp` — custom TUI rendering engine using low-level `Console` APIs and a Windows-specific `ConsoleInputReader` (reads raw input events). Manages a scrollable log of `NotificationEntry` items and a single-line input bar.
|
||||
- `DeepSeekLlmSession` — LLM integration via `Microsoft.Agents.AI.OpenAI`. Configured from `raman-agent.toml` (TOML format, loaded from app base directory). Environment variables `DEEPSEEK_API_KEY` and `DEEPSEEK_MODEL` override the config file.
|
||||
|
||||
**RamanTester** — Standalone x86 console app for direct Sea SDK testing. Targets x86 (`PlatformTarget=x86`) because the native Sea SDK DLL is 32-bit.
|
||||
|
||||
### Key patterns
|
||||
|
||||
- Commands flow as: user text -> `ICommandSession.ExecuteAsync` -> `*RuntimeWorkspace.HandleAsync` -> `ISpectrometerDevice`/`IFilterWheelDevice` method.
|
||||
- All command sessions return `IReadOnlyList<NotificationEntry>` which the TUI renders as log lines.
|
||||
- Background commands use `publishAsync` callback to stream entries to the UI incrementally.
|
||||
- The LLM agent uses tool calling — `TuiLlmCommandTool.RunTuiCommandAsync` executes TUI commands on behalf of the LLM and returns results as text.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- File-scoped namespaces (`csharp_style_namespace_declarations = file_scoped`).
|
||||
- LF line endings, UTF-8, spaces for indentation.
|
||||
- 2-space indent for XML/csproj files.
|
||||
- Commit messages are in Chinese.
|
||||
- The project language mixes English (code) and Chinese (UI strings, LLM instructions, commit messages).
|
||||
+5
-3
@@ -2,11 +2,13 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<!--<add key="nuget-cdn" value="https://nuget.cdn.azure.cn/v3/index.json" allowInsecureConnections="True" />-->
|
||||
<add key="nuget-src" value="https://api.nuget.org/v3/index.json" allowInsecureConnections="True" />
|
||||
<add key="nuget-cdn" value="https://repo.huaweicloud.com/repository/nuget/v3/index.json"
|
||||
allowInsecureConnections="True" />
|
||||
<add key="nuget-src" value="https://api.nuget.org/v3/index.json"
|
||||
allowInsecureConnections="True" />
|
||||
</packageSources>
|
||||
<disabledPackageSources>
|
||||
<clear />
|
||||
</disabledPackageSources>
|
||||
|
||||
</configuration>
|
||||
</configuration>
|
||||
@@ -3,6 +3,7 @@
|
||||
<Project Path="src/Raman.Core/Raman.Core.csproj" Id="b8a8d619-9e32-4df0-ab7d-33398f956c2a" />
|
||||
<Project Path="src/Raman.Simulator/Raman.Simulator.csproj" Id="42c899c8-76e7-4fa0-82d3-8842430f0512" />
|
||||
<Project Path="src/Raman.TUI/Raman.TUI.csproj" Id="a5af998c-7236-4d76-94ed-a6d236e43142" />
|
||||
<Project Path="src/RamanApp/RamanApp.csproj" Id="13b5601c-c4e1-48f2-b2ad-e613589f465a" />
|
||||
<Project Path="src/RamanTester/RamanTester.csproj" Id="7ab838ce-add8-464b-8a16-e9b8b709852f" />
|
||||
<Project Path="src/Roman.Host/Roman.Host.csproj" Id="d1e35ba3-866e-41eb-b530-587f06a10e11" />
|
||||
<Project Path="src/Spec.Sea/Spec.Sea.csproj" Id="06a9c31a-66ff-4fbe-be5a-35533c0ad249" />
|
||||
|
||||
@@ -165,7 +165,7 @@ internal sealed class DeepSeekLlmSession : ITuiLlmSession
|
||||
The tool only supports commands that start with filter or spec.
|
||||
Never invent device lists, aliases, connection states, positions, spectra, or execution results.
|
||||
If device state or configuration is needed, call filter show, filter list, spec status, or spec scan first as appropriate.
|
||||
For spectrometer acquisition configuration, first call spec show <device> and read the integration_us min..max range. Only call spec config <device> with an explicit --integration value inside that range.
|
||||
For spectrometer acquisition configuration, first call spec show <device> and read the integration_us min..max range. The spec config --integration argument is always microseconds/us. If the user says ms or seconds, convert to microseconds before calling spec config. Only call spec config <device> with an explicit --integration value inside that range.
|
||||
Never call spec config only to inspect current configuration; use spec show, spec diagnostics, or spec processing <device> status instead.
|
||||
For control actions such as connect, disconnect, move, acquire, or configuration, state the action briefly, call the tool, then summarize the result based only on tool output.
|
||||
If required parameters are missing, ask for the missing parameter before executing.
|
||||
@@ -197,7 +197,7 @@ internal sealed class DeepSeekLlmSession : ITuiLlmSession
|
||||
spec connect <device>
|
||||
spec disconnect <device>
|
||||
spec acquire <device>
|
||||
spec config <device> ...
|
||||
spec config <device> --integration <microseconds> [--average <count>] [--boxcar <pixels>]
|
||||
spec diagnostics <device>
|
||||
""";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Raman.TUI;
|
||||
|
||||
internal sealed class SpectrometerCommandSession : ICommandSession
|
||||
{
|
||||
private const string UsageConfig = "spec config <device> --integration 50000 [--average 1] [--boxcar 0]";
|
||||
private const string UsageConfig = "spec config <device> --integration <microseconds> [--average 1] [--boxcar 0]";
|
||||
private const string UsageAcquire = "spec acquire <device>";
|
||||
private const string UsageHighSpeed = "spec highspeed <device> --frames 10";
|
||||
private const string UsageProcessing = "spec processing <device> status|set [--boxcar-mode 0] [--stretch-mode 0] [--long true|false] [--saturation-us 0] [--pixels 2048] [--usb-protocol 0] [--serial-protocol 0]";
|
||||
@@ -602,7 +602,7 @@ internal sealed class SpectrometerCommandSession : ICommandSession
|
||||
("spec show [device]", "Show all spectrometer statuses or one device status."),
|
||||
("spec connect <device>", "Connect a discovered spectrometer."),
|
||||
("spec disconnect <device>", "Disconnect a connected spectrometer."),
|
||||
("spec config <device>", "Set acquisition parameters. Use --integration, --average, --boxcar."),
|
||||
("spec config <device>", "Set acquisition parameters. --integration is in microseconds/us. Use --average and --boxcar as needed."),
|
||||
("spec acquire <device>", "Acquire one formatted spectrum frame."),
|
||||
("spec highspeed <device> --frames 10", "Acquire a high-speed spectrum batch."),
|
||||
("spec diagnostics <device>", "Read SDK and device diagnostics."),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<Application
|
||||
x:Class="RamanApp.App"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
RequestedThemeVariant="Light">
|
||||
|
||||
<Application.Styles>
|
||||
<semi:SemiTheme Locale="zh-CN" />
|
||||
<semi:DataGridSemiTheme />
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI, Microsoft YaHei, SimHei" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="RadioButton">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="ToggleSwitch">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,24 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace RamanApp;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<Window
|
||||
x:Class="RamanApp.MainWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:views="using:RamanApp.Views"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
Title="拉曼光谱仪"
|
||||
Width="1280"
|
||||
Height="800"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Menu bar -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
BorderBrush="{DynamicResource SemiColorBorder}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Menu>
|
||||
<MenuItem Header="文件">
|
||||
<MenuItem Header="导入光谱数据" />
|
||||
<MenuItem Header="导出光谱数据" />
|
||||
<MenuItem Header="拷贝光谱数据" />
|
||||
<Separator />
|
||||
<MenuItem Header="光谱存为图片" />
|
||||
<MenuItem Header="光谱存为PDF" />
|
||||
<Separator />
|
||||
<MenuItem Header="退出" Click="OnExitClick" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="显示设置">
|
||||
<MenuItem Header="显示图例">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.ShowLegend}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="显示峰位X值">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakX}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="显示峰位Y值">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakY}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="显示峰位直线">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakLine}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="X轴坐标翻转显示">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.InvertXAxis}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="填充谱峰">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.FillPeaks}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="所有图谱归一化显示[最大10000]">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding DisplaySettings.NormalizeAll}"
|
||||
BorderThickness="0" MinWidth="0" Padding="0" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="修改辐射坐标单位" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="帮助">
|
||||
<MenuItem Header="关于" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Border>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Border
|
||||
Padding="4,2"
|
||||
Background="{DynamicResource SemiColorFillQuaternary}"
|
||||
BorderBrush="{DynamicResource SemiColorBorder}"
|
||||
BorderThickness="0,0,0,1"
|
||||
DockPanel.Dock="Top">
|
||||
|
||||
<Border.Styles>
|
||||
<Style Selector="Button.tool-btn">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="6,4" />
|
||||
<Setter Property="MinWidth" Value="52" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Button.tool-btn PathIcon">
|
||||
<Setter Property="Width" Value="18" />
|
||||
<Setter Property="Height" Value="18" />
|
||||
</Style>
|
||||
<Style Selector="Button.tool-btn TextBlock">
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</Border.Styles>
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Right side: progress bar + status text -->
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"
|
||||
Spacing="8" VerticalAlignment="Center" Margin="8,0">
|
||||
<TextBlock Text="进度:" VerticalAlignment="Center" Foreground="Gray" FontSize="10" />
|
||||
<ProgressBar Value="100" Minimum="0" Maximum="100"
|
||||
Width="140" Height="14" VerticalAlignment="Center"
|
||||
Foreground="#4CAF50" />
|
||||
<TextBlock Text="100%" VerticalAlignment="Center" FontSize="10" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
|
||||
<!-- 单次 -->
|
||||
<Button Classes="tool-btn" Command="{Binding Acquisition.SingleAcquireCommand}">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#4CAF50"
|
||||
Data="M8,5.14V19.14L19,12.14L8,5.14Z" />
|
||||
<TextBlock Text="单次" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 连续 -->
|
||||
<Button Classes="tool-btn" Command="{Binding Acquisition.ContinuousAcquireCommand}">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#FF9800"
|
||||
Data="M17.65,6.35A7.958,7.958 0 0,0 12,4C7.58,4 4.01,7.58 4.01,12C4.01,16.42 7.58,20 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18C8.69,18 6,15.31 6,12C6,8.69 8.69,6 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" />
|
||||
<TextBlock Text="连续" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
|
||||
|
||||
<!-- 保存明光谱 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#FFC107"
|
||||
Data="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
|
||||
<TextBlock Text="保存明光谱" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 保存暗光谱 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#607D8B"
|
||||
Data="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
|
||||
<TextBlock Text="保存暗光谱" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
|
||||
|
||||
<!-- 最大化 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#2196F3"
|
||||
Data="M9.5,13.09L10.91,14.5L6.41,19H10V21H3V14H5V17.59L9.5,13.09M10.91,9.5L9.5,10.91L5,6.41V10H3V3H10V5H6.41L10.91,9.5M14.5,13.09L19,17.59V14H21V21H14V19H17.59L13.09,14.5L14.5,13.09M13.09,9.5L17.59,5H14V3H21V10H19V6.41L14.5,10.91L13.09,9.5Z" />
|
||||
<TextBlock Text="最大化" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 自适应显示 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#00BCD4"
|
||||
Data="M17,4H7A2,2 0 0,0 5,6V18A2,2 0 0,0 7,20H17A2,2 0 0,0 19,18V6A2,2 0 0,0 17,4M7,18V6H17V18H7M15,8H9V10H15V8M15,12H9V14H15V12Z" />
|
||||
<TextBlock Text="自适应显示" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 自定义 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#4CAF50"
|
||||
Data="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
|
||||
<TextBlock Text="自定义" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
|
||||
|
||||
<!-- 快速保存 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#2196F3"
|
||||
Data="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
|
||||
<TextBlock Text="快速保存" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 导出光谱数据 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#FF5722"
|
||||
Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M12,19L8,15H10.5V12H13.5V15H16L12,19Z" />
|
||||
<TextBlock Text="导出光谱数据" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 导入光谱数据 -->
|
||||
<Button Classes="tool-btn">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#3F51B5"
|
||||
Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M12,12L16,16H13.5V19H10.5V16H8L12,12Z" />
|
||||
<TextBlock Text="导入光谱数据" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Bottom status bar -->
|
||||
<Border
|
||||
Padding="8,3"
|
||||
Background="{DynamicResource SemiColorFillQuaternary}"
|
||||
BorderBrush="{DynamicResource SemiColorBorder}"
|
||||
BorderThickness="0,1,0,0"
|
||||
DockPanel.Dock="Bottom">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
Text="{Binding DeviceSettings.Spectrometer1.SelectedDevice, StringFormat=设备1: {0}, FallbackValue=设备1: (未连接)}" />
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
Text="{Binding DeviceSettings.Spectrometer1.IntegrationTimeMs, StringFormat=积分: {0}ms}" />
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
Text="{Binding DeviceSettings.Spectrometer1.AverageCount, StringFormat=平均: {0}次}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
Text="{Binding Acquisition.StatusText}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid ColumnDefinitions="240,Auto,*,Auto,260">
|
||||
|
||||
<!-- Left panel: settings + data processing tabs (resizable) -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
MinWidth="180"
|
||||
BorderBrush="{DynamicResource SemiColorBorder}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<TabControl TabStripPlacement="Top" Padding="0">
|
||||
<TabItem Header="光谱仪">
|
||||
<ScrollViewer Padding="8">
|
||||
<views:DeviceSettingsView DataContext="{Binding DeviceSettings}" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="激光器">
|
||||
<ScrollViewer Padding="8">
|
||||
<views:LaserSettingsView DataContext="{Binding LaserSettings}" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="数据处理">
|
||||
<views:DataProcessingView DataContext="{Binding DataProcessing}" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
<!-- GridSplitter between left panel and center -->
|
||||
<GridSplitter Grid.Column="1" Width="4" ResizeDirection="Columns"
|
||||
Background="Transparent" />
|
||||
|
||||
<!-- Center: Spectrum chart tabs -->
|
||||
<TabControl Grid.Column="2" Padding="0" Margin="4,0">
|
||||
<TabItem Header="原始图谱">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="亮/暗光谱">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="扣除背景">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="光谱仪 1">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="光谱仪 2">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="合并光谱">
|
||||
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- GridSplitter between center and right panel -->
|
||||
<GridSplitter Grid.Column="3" Width="4" ResizeDirection="Columns"
|
||||
Background="Transparent" />
|
||||
|
||||
<!-- Right panel: peak detection + coordinate + control tabs (resizable) -->
|
||||
<Border
|
||||
Grid.Column="4"
|
||||
MinWidth="180"
|
||||
BorderBrush="{DynamicResource SemiColorBorder}"
|
||||
BorderThickness="1,0,0,0">
|
||||
<TabControl TabStripPlacement="Top" Padding="0">
|
||||
<TabItem Header="峰位">
|
||||
<views:PeakDetectionView DataContext="{Binding PeakDetection}" Padding="8" />
|
||||
</TabItem>
|
||||
<TabItem Header="坐标">
|
||||
<views:CoordinateView DataContext="{Binding Coordinate}" Padding="8" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using RamanApp.ViewModels;
|
||||
|
||||
namespace RamanApp;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var vm = new MainWindowViewModel();
|
||||
DataContext = vm;
|
||||
vm.SpectrumChart.LoadDemoData();
|
||||
}
|
||||
|
||||
private void OnExitClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace RamanApp;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<!--<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.17" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.17" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.17" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.17" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.17">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="Irihi.Ursa" Version="1.15.1" />
|
||||
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="1.15.1" />
|
||||
<PackageReference Include="ScottPlot.Avalonia" Version="5.1.58" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="11.3.14" />
|
||||
<PackageReference Include="Semi.Avalonia.DataGrid" Version="11.3.7.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,53 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class AcquisitionViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial string AcquisitionMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int ContinuousIntervalMs { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSingleAcquire { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int ContinuousCount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsAcquiring { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusText { get; set; }
|
||||
|
||||
public AcquisitionViewModel()
|
||||
{
|
||||
AcquisitionMode = "普通模式";
|
||||
IsSingleAcquire = true;
|
||||
ContinuousCount = 1;
|
||||
StatusText = "就绪";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SingleAcquire()
|
||||
{
|
||||
StatusText = "单次采集中...";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ContinuousAcquire()
|
||||
{
|
||||
IsAcquiring = !IsAcquiring;
|
||||
StatusText = IsAcquiring ? "连续采集中..." : "就绪";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void StopAcquire()
|
||||
{
|
||||
IsAcquiring = false;
|
||||
StatusText = "就绪";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class CoordinateViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CoordinateEntry> Entries { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string LookupValue { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double PeakWavelength { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double PeakFwhm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double AreaStart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double AreaEnd { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double PeakArea { get; set; }
|
||||
|
||||
public CoordinateViewModel()
|
||||
{
|
||||
Entries = [];
|
||||
LookupValue = "1";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Lookup() { }
|
||||
|
||||
[RelayCommand]
|
||||
private void SearchLeft() { }
|
||||
|
||||
[RelayCommand]
|
||||
private void SearchRight() { }
|
||||
|
||||
[RelayCommand]
|
||||
private void CalculateArea() { }
|
||||
}
|
||||
|
||||
public partial class CoordinateEntry : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial string Name { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double WavelengthNm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double Intensity { get; set; }
|
||||
|
||||
public CoordinateEntry()
|
||||
{
|
||||
Name = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class DataProcessingViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool AutoProcessAfterAcquire { get; set; }
|
||||
|
||||
// 1 - Interpolation & trimming
|
||||
[ObservableProperty]
|
||||
public partial bool InterpolationEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string InterpolationMethod { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int InterpolationInterval { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double InterpolationStart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double InterpolationEnd { get; set; }
|
||||
|
||||
// 2 - Baseline correction
|
||||
[ObservableProperty]
|
||||
public partial bool BaselineCorrectionEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string BaselineMethod { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BaselineLambda { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BaselineOrder { get; set; }
|
||||
|
||||
// 3 - Smoothing
|
||||
[ObservableProperty]
|
||||
public partial bool SmoothingEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SmoothingMethod { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double SmoothingLambda { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double SmoothingOrder { get; set; }
|
||||
|
||||
// 4 - Spectral derivative
|
||||
[ObservableProperty]
|
||||
public partial bool DerivativeEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string DerivativeMethod { get; set; }
|
||||
|
||||
// 5 - Normalization
|
||||
[ObservableProperty]
|
||||
public partial bool NormalizationEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string NormalizationMethod { get; set; }
|
||||
|
||||
public DataProcessingViewModel()
|
||||
{
|
||||
InterpolationMethod = "线性插值";
|
||||
InterpolationInterval = 1;
|
||||
InterpolationStart = 200;
|
||||
InterpolationEnd = 3100;
|
||||
|
||||
BaselineMethod = "airPLS";
|
||||
BaselineLambda = 100000;
|
||||
BaselineOrder = 2.00;
|
||||
|
||||
SmoothingEnabled = true;
|
||||
SmoothingMethod = "Whittaker平滑";
|
||||
SmoothingLambda = 15;
|
||||
SmoothingOrder = 1.00;
|
||||
|
||||
DerivativeMethod = "一阶导数";
|
||||
|
||||
NormalizationMethod = "最大值归一化";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void PreviewSelected()
|
||||
{
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void PreviewAll()
|
||||
{
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetProcessing()
|
||||
{
|
||||
InterpolationEnabled = false;
|
||||
BaselineCorrectionEnabled = false;
|
||||
SmoothingEnabled = true;
|
||||
DerivativeEnabled = false;
|
||||
NormalizationEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class DeviceSettingsViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial SpectrometerGroupViewModel Spectrometer1 { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial SpectrometerGroupViewModel Spectrometer2 { get; set; }
|
||||
|
||||
public DeviceSettingsViewModel()
|
||||
{
|
||||
Spectrometer1 = new SpectrometerGroupViewModel("光谱仪 1");
|
||||
Spectrometer2 = new SpectrometerGroupViewModel("光谱仪 2");
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SpectrometerGroupViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial string GroupName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<string> Devices { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? SelectedDevice { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int IntegrationTimeMs { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int AverageCount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BoxcarWidth { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool DarkNoiseEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool NonlinearityCorrectionEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int TriggerTimeoutMs { get; set; }
|
||||
|
||||
public SpectrometerGroupViewModel(string groupName)
|
||||
{
|
||||
GroupName = groupName;
|
||||
Devices = ["(未连接)"];
|
||||
IntegrationTimeMs = 1;
|
||||
AverageCount = 1;
|
||||
TriggerTimeoutMs = 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class DisplaySettingsViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool ShowLegend { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowPeakX { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowPeakY { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowPeakLine { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool InvertXAxis { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool FillPeaks { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool NormalizeAll { get; set; }
|
||||
|
||||
public DisplaySettingsViewModel()
|
||||
{
|
||||
ShowLegend = true;
|
||||
ShowPeakX = true;
|
||||
ShowPeakY = true;
|
||||
ShowPeakLine = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class LaserSettingsViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool LaserEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double LaserPowerPercent { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double LaserWavelengthNm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool AutoLaserOnAcquire { get; set; }
|
||||
|
||||
public LaserSettingsViewModel()
|
||||
{
|
||||
LaserPowerPercent = 100;
|
||||
LaserWavelengthNm = 785;
|
||||
AutoLaserOnAcquire = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleLaser()
|
||||
{
|
||||
LaserEnabled = !LaserEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial DeviceSettingsViewModel DeviceSettings { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial LaserSettingsViewModel LaserSettings { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AcquisitionViewModel Acquisition { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial SpectrumChartViewModel SpectrumChart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial PeakDetectionViewModel PeakDetection { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CoordinateViewModel Coordinate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DataProcessingViewModel DataProcessing { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DisplaySettingsViewModel DisplaySettings { get; set; }
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
DeviceSettings = new DeviceSettingsViewModel();
|
||||
LaserSettings = new LaserSettingsViewModel();
|
||||
Acquisition = new AcquisitionViewModel();
|
||||
SpectrumChart = new SpectrumChartViewModel();
|
||||
PeakDetection = new PeakDetectionViewModel();
|
||||
Coordinate = new CoordinateViewModel();
|
||||
DataProcessing = new DataProcessingViewModel();
|
||||
DisplaySettings = new DisplaySettingsViewModel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class PeakDetectionViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool PeakDetectionEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double Threshold { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int MinPeakDistance { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<PeakEntry> Peaks { get; set; }
|
||||
|
||||
public PeakDetectionViewModel()
|
||||
{
|
||||
Threshold = 1000;
|
||||
MinPeakDistance = 5;
|
||||
Peaks = [];
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DetectPeaks()
|
||||
{
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ExportPeaks()
|
||||
{
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearPeaks()
|
||||
{
|
||||
Peaks.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PeakEntry : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial double WavelengthNm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double Intensity { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double WavenumberCm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double CenterWavelengthNm { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace RamanApp.ViewModels;
|
||||
|
||||
public partial class SpectrumChartViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial string XAxisMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double[] XData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double[] YData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string XAxisLabel { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string YAxisLabel { get; set; }
|
||||
|
||||
public SpectrumChartViewModel()
|
||||
{
|
||||
XAxisMode = "波长(nm)";
|
||||
XData = [];
|
||||
YData = [];
|
||||
XAxisLabel = "Wavelength (nm)";
|
||||
YAxisLabel = "Intensity";
|
||||
}
|
||||
|
||||
public void UpdateSpectrum(double[] x, double[] y)
|
||||
{
|
||||
XData = x;
|
||||
YData = y;
|
||||
}
|
||||
|
||||
public void LoadDemoData()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var x = new double[2048];
|
||||
var y = new double[2048];
|
||||
for (var i = 0; i < 2048; i++)
|
||||
{
|
||||
x[i] = 200 + i * 0.7;
|
||||
y[i] = rng.NextDouble() * 200;
|
||||
}
|
||||
|
||||
y[700] = 60000;
|
||||
y[701] = 45000;
|
||||
y[699] = 35000;
|
||||
|
||||
XData = x;
|
||||
YData = y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:Class="RamanApp.Views.AcquisitionView"
|
||||
x:DataType="vm:AcquisitionViewModel">
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<Grid ColumnDefinitions="90,*">
|
||||
<TextBlock Text="采集模式" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Column="1" SelectedItem="{Binding AcquisitionMode}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBoxItem Content="普通模式" />
|
||||
<ComboBoxItem Content="高速模式" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="90,*">
|
||||
<TextBlock Text="连续间隔(ms)" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Column="1" Value="{Binding ContinuousIntervalMs}"
|
||||
Minimum="0" Maximum="60000" Increment="100" FormatString="0" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" Margin="0,2">
|
||||
<RadioButton Content="每次采集" IsChecked="{Binding IsSingleAcquire}"
|
||||
GroupName="AcquireMode" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
|
||||
<RadioButton Content="固定次数" GroupName="AcquireMode" />
|
||||
<NumericUpDown Value="{Binding ContinuousCount}"
|
||||
Minimum="1" Maximum="9999" Increment="1"
|
||||
FormatString="0" Width="70" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
|
||||
<Button Content="单次" Command="{Binding SingleAcquireCommand}"
|
||||
Width="60" HorizontalContentAlignment="Center" />
|
||||
<Button Content="连续" Command="{Binding ContinuousAcquireCommand}"
|
||||
Width="60" HorizontalContentAlignment="Center" />
|
||||
<Button Content="停止" Command="{Binding StopAcquireCommand}"
|
||||
Width="60" HorizontalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="Gray"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class AcquisitionView : UserControl
|
||||
{
|
||||
public AcquisitionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<UserControl
|
||||
x:Class="RamanApp.Views.CoordinateView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:DataType="vm:CoordinateViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Spacing="8" Margin="0,8,0,0">
|
||||
|
||||
<!-- Lookup coordinate -->
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="查看" Margin="4,0,0,0"
|
||||
Command="{Binding LookupCommand}" />
|
||||
<TextBlock DockPanel.Dock="Left" Text="查看指定坐标数值"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Text="{Binding LookupValue}" />
|
||||
</DockPanel>
|
||||
|
||||
<Separator Margin="0,0" />
|
||||
|
||||
<!-- Search buttons -->
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0" Content="<<— 向左寻峰" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Margin="0,0,2,0"
|
||||
Command="{Binding SearchLeftCommand}" />
|
||||
<Button Grid.Column="1" Content="向右寻峰 —>>" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Margin="2,0,0,0"
|
||||
Command="{Binding SearchRightCommand}" />
|
||||
</Grid>
|
||||
|
||||
<Separator Margin="0,0" />
|
||||
|
||||
<!-- Peak position -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="峰位 [右键选择]" FontWeight="SemiBold" />
|
||||
<DockPanel>
|
||||
<TextBlock Text="波长:" VerticalAlignment="Center" Width="70" />
|
||||
<TextBox IsReadOnly="True" Text="{Binding PeakWavelength}" />
|
||||
</DockPanel>
|
||||
<DockPanel>
|
||||
<TextBlock Text="半峰宽(nm):" VerticalAlignment="Center" Width="70" />
|
||||
<TextBox IsReadOnly="True" Text="{Binding PeakFwhm}" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,0" />
|
||||
|
||||
<!-- Peak area -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="计算峰面积" FontWeight="SemiBold" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="起始拉曼"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding AreaStart}" Margin="0,0,8,0" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="结束拉曼"
|
||||
VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding AreaEnd}" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="峰面积"
|
||||
VerticalAlignment="Center" Margin="0,4,4,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
IsReadOnly="True" Text="{Binding PeakArea}" Margin="0,4,8,0" />
|
||||
<Button Grid.Row="1" Grid.Column="3" Content="计算" Margin="0,4,0,0"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Command="{Binding CalculateAreaCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Top: data table -->
|
||||
<DataGrid ItemsSource="{Binding Entries}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
HeadersVisibility="Column"
|
||||
GridLinesVisibility="All">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="*" />
|
||||
<DataGridTextColumn Header="波长(nm)" Binding="{Binding WavelengthNm, StringFormat={}{0:F3}}" Width="*" />
|
||||
<DataGridTextColumn Header="峰强" Binding="{Binding Intensity, StringFormat={}{0:F0}}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class CoordinateView : UserControl
|
||||
{
|
||||
public CoordinateView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:Class="RamanApp.Views.DataProcessingView"
|
||||
x:DataType="vm:DataProcessingViewModel">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Bottom buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
|
||||
Spacing="6" HorizontalAlignment="Center" Margin="0,8,0,4">
|
||||
<Button Content="预览选中" Command="{Binding PreviewSelectedCommand}" />
|
||||
<Button Content="预览全部" Command="{Binding PreviewAllCommand}" />
|
||||
<Button Content="重置" Command="{Binding ResetProcessingCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Padding="8,4">
|
||||
<StackPanel Spacing="6">
|
||||
<CheckBox Content="采集后自动处理" IsChecked="{Binding AutoProcessAfterAcquire}" />
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- 1 - Interpolation -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Content="1 - 插值和剪切" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}"
|
||||
IsChecked="{Binding InterpolationEnabled}" />
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
|
||||
IsEnabled="{Binding InterpolationEnabled}" Margin="20,0,0,0">
|
||||
<TextBlock Grid.Row="0" Text="插值算法" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding InterpolationMethod}"
|
||||
HorizontalAlignment="Stretch" Margin="0,0,0,3">
|
||||
<ComboBoxItem Content="线性插值" />
|
||||
<ComboBoxItem Content="三次样条" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="插值间隔" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding InterpolationInterval}"
|
||||
Minimum="1" Maximum="100" FormatString="0" Margin="0,0,0,3" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="范围" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Spacing="4">
|
||||
<NumericUpDown Value="{Binding InterpolationStart}"
|
||||
Minimum="0" Maximum="10000" FormatString="0" Width="80" />
|
||||
<TextBlock Text="~" VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding InterpolationEnd}"
|
||||
Minimum="0" Maximum="10000" FormatString="0" Width="80" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- 2 - Baseline -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Content="2 - 基线处理" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}"
|
||||
IsChecked="{Binding BaselineCorrectionEnabled}" />
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
|
||||
IsEnabled="{Binding BaselineCorrectionEnabled}" Margin="20,0,0,0">
|
||||
<TextBlock Grid.Row="0" Text="处理算法" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding BaselineMethod}"
|
||||
HorizontalAlignment="Stretch" Margin="0,0,0,3">
|
||||
<ComboBoxItem Content="airPLS" />
|
||||
<ComboBoxItem Content="arPLS" />
|
||||
<ComboBoxItem Content="多项式拟合" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="lambda" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding BaselineLambda}"
|
||||
Minimum="1" Maximum="10000000" FormatString="0" Margin="0,0,0,3" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="阶数" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding BaselineOrder}"
|
||||
Minimum="0.01" Maximum="10" Increment="0.5" FormatString="0.00" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- 3 - Smoothing -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Content="3 - 平滑滤波" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}"
|
||||
IsChecked="{Binding SmoothingEnabled}" />
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
|
||||
IsEnabled="{Binding SmoothingEnabled}" Margin="20,0,0,0">
|
||||
<TextBlock Grid.Row="0" Text="平滑算法" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding SmoothingMethod}"
|
||||
HorizontalAlignment="Stretch" Margin="0,0,0,3">
|
||||
<ComboBoxItem Content="Whittaker" />
|
||||
<ComboBoxItem Content="Savitzky-Golay" />
|
||||
<ComboBoxItem Content="移动平均" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="lambda" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding SmoothingLambda}"
|
||||
Minimum="1" Maximum="10000" FormatString="0" Margin="0,0,0,3" />
|
||||
|
||||
<TextBlock Grid.Row="2" Text="阶数" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding SmoothingOrder}"
|
||||
Minimum="0.01" Maximum="10" Increment="0.5" FormatString="0.00" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- 4 - Derivative -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Content="4 - 光谱导数" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}"
|
||||
IsChecked="{Binding DerivativeEnabled}" />
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto"
|
||||
IsEnabled="{Binding DerivativeEnabled}" Margin="20,0,0,0">
|
||||
<TextBlock Grid.Row="0" Text="处理方法" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding DerivativeMethod}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBoxItem Content="一阶导数" />
|
||||
<ComboBoxItem Content="二阶导数" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- 5 - Normalization -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Content="5 - 光谱归一化" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}"
|
||||
IsChecked="{Binding NormalizationEnabled}" />
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto"
|
||||
IsEnabled="{Binding NormalizationEnabled}" Margin="20,0,0,0">
|
||||
<TextBlock Grid.Row="0" Text="处理方法" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding NormalizationMethod}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBoxItem Content="最大值归一化" />
|
||||
<ComboBoxItem Content="面积归一化" />
|
||||
<ComboBoxItem Content="向量归一化" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class DataProcessingView : UserControl
|
||||
{
|
||||
public DataProcessingView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:Class="RamanApp.Views.DeviceSettingsView"
|
||||
x:DataType="vm:DeviceSettingsViewModel">
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<!-- Spectrometer 1 -->
|
||||
<HeaderedContentControl>
|
||||
<HeaderedContentControl.Header>
|
||||
<TextBlock Text="{Binding Spectrometer1.GroupName}" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}" />
|
||||
</HeaderedContentControl.Header>
|
||||
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
Margin="0,4,0,0">
|
||||
<TextBlock Grid.Row="0" Text="光谱仪" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Spectrometer1.Devices}"
|
||||
SelectedItem="{Binding Spectrometer1.SelectedDevice}"
|
||||
HorizontalAlignment="Stretch" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="积分时间" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding Spectrometer1.IntegrationTimeMs}"
|
||||
Minimum="1" Maximum="5000" Increment="1" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="平均次数" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding Spectrometer1.AverageCount}"
|
||||
Minimum="1" Maximum="1000" Increment="1" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="次" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<TextBlock Grid.Row="3" Text="Boxcar" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding Spectrometer1.BoxcarWidth}"
|
||||
Minimum="0" Maximum="100" Increment="1" FormatString="0" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Grid.Row="4" Text="触发超时" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="4" Grid.Column="1" Value="{Binding Spectrometer1.TriggerTimeoutMs}"
|
||||
Minimum="0" Maximum="60000" Increment="10" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<CheckBox Grid.Row="5" Grid.ColumnSpan="2" Content="暗噪声校准"
|
||||
IsChecked="{Binding Spectrometer1.DarkNoiseEnabled}" Margin="0,0,0,2" />
|
||||
<CheckBox Grid.Row="6" Grid.ColumnSpan="2" Content="非线性校准"
|
||||
IsChecked="{Binding Spectrometer1.NonlinearityCorrectionEnabled}" />
|
||||
</Grid>
|
||||
</HeaderedContentControl>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- Spectrometer 2 -->
|
||||
<HeaderedContentControl>
|
||||
<HeaderedContentControl.Header>
|
||||
<TextBlock Text="{Binding Spectrometer2.GroupName}" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SemiColorPrimary}" />
|
||||
</HeaderedContentControl.Header>
|
||||
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
Margin="0,4,0,0">
|
||||
<TextBlock Grid.Row="0" Text="光谱仪" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Spectrometer2.Devices}"
|
||||
SelectedItem="{Binding Spectrometer2.SelectedDevice}"
|
||||
HorizontalAlignment="Stretch" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="积分时间" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding Spectrometer2.IntegrationTimeMs}"
|
||||
Minimum="1" Maximum="5000" Increment="1" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="平均次数" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding Spectrometer2.AverageCount}"
|
||||
Minimum="1" Maximum="1000" Increment="1" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="次" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<TextBlock Grid.Row="3" Text="Boxcar" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding Spectrometer2.BoxcarWidth}"
|
||||
Minimum="0" Maximum="100" Increment="1" FormatString="0" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Grid.Row="4" Text="触发超时" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="4" Grid.Column="1" Value="{Binding Spectrometer2.TriggerTimeoutMs}"
|
||||
Minimum="0" Maximum="60000" Increment="10" FormatString="0" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<CheckBox Grid.Row="5" Grid.ColumnSpan="2" Content="暗噪声校准"
|
||||
IsChecked="{Binding Spectrometer2.DarkNoiseEnabled}" Margin="0,0,0,2" />
|
||||
<CheckBox Grid.Row="6" Grid.ColumnSpan="2" Content="非线性校准"
|
||||
IsChecked="{Binding Spectrometer2.NonlinearityCorrectionEnabled}" />
|
||||
</Grid>
|
||||
</HeaderedContentControl>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class DeviceSettingsView : UserControl
|
||||
{
|
||||
public DeviceSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<UserControl
|
||||
x:Class="RamanApp.Views.LaserSettingsView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:DataType="vm:LaserSettingsViewModel">
|
||||
|
||||
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock Grid.Row="0" Text="激光器开关" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<ToggleSwitch Grid.Row="0" Grid.Column="1" IsChecked="{Binding LaserEnabled}"
|
||||
OnContent="开" OffContent="关" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="激光功率" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding LaserPowerPercent}"
|
||||
Minimum="0" Maximum="100" Increment="5" FormatString="0.#" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="%" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="激光波长" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding LaserWavelengthNm}"
|
||||
Minimum="200" Maximum="1200" Increment="1"
|
||||
FormatString="0.#" IsReadOnly="True" Margin="0,0,0,4">
|
||||
<NumericUpDown.InnerRightContent>
|
||||
<TextBlock Text="nm" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
</NumericUpDown.InnerRightContent>
|
||||
</NumericUpDown>
|
||||
|
||||
<CheckBox Grid.Row="3" Grid.ColumnSpan="2" Content="采集时自动开启激光"
|
||||
IsChecked="{Binding AutoLaserOnAcquire}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class LaserSettingsView : UserControl
|
||||
{
|
||||
public LaserSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:Class="RamanApp.Views.PeakDetectionView"
|
||||
x:DataType="vm:PeakDetectionViewModel">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Bottom buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
|
||||
Spacing="8" HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||
<Button Content="导出" Command="{Binding ExportPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
|
||||
<Button Content="删除" Command="{Binding ClearPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
|
||||
<Button Content="重置" Command="{Binding DetectPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<CheckBox Content="峰检测" IsChecked="{Binding PeakDetectionEnabled}" />
|
||||
|
||||
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Text="阈值" Grid.Row="0" VerticalAlignment="Center" Margin="0,0,8,4" />
|
||||
<NumericUpDown Grid.Row="0" Grid.Column="1"
|
||||
Value="{Binding Threshold}"
|
||||
Minimum="0" Maximum="100000"
|
||||
Increment="100" FormatString="0" />
|
||||
|
||||
<TextBlock Text="最小峰距" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1"
|
||||
Value="{Binding MinPeakDistance}"
|
||||
Minimum="1" Maximum="500"
|
||||
Increment="1" FormatString="0" />
|
||||
</Grid>
|
||||
|
||||
<Separator Margin="0,2" />
|
||||
|
||||
<!-- Peak table -->
|
||||
<DataGrid ItemsSource="{Binding Peaks}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
HeadersVisibility="Column"
|
||||
GridLinesVisibility="Horizontal"
|
||||
MinHeight="120">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="波长(nm)" Binding="{Binding WavelengthNm, StringFormat={}{0:0.###}}" Width="*" />
|
||||
<DataGridTextColumn Header="强度" Binding="{Binding Intensity, StringFormat={}{0:0.#}}" Width="*" />
|
||||
<DataGridTextColumn Header="波数(cm⁻¹)" Binding="{Binding WavenumberCm, StringFormat={}{0:0.#}}" Width="*" />
|
||||
<DataGridTextColumn Header="中心波长(nm)" Binding="{Binding CenterWavelengthNm, StringFormat={}{0:0.###}}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class PeakDetectionView : UserControl
|
||||
{
|
||||
public PeakDetectionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<UserControl
|
||||
x:Class="RamanApp.Views.SpectrumChartView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sp="clr-namespace:ScottPlot.Avalonia;assembly=ScottPlot.Avalonia"
|
||||
xmlns:vm="using:RamanApp.ViewModels"
|
||||
x:DataType="vm:SpectrumChartViewModel">
|
||||
|
||||
<DockPanel>
|
||||
<!-- X axis mode toggle at bottom -->
|
||||
<StackPanel
|
||||
Margin="0,6"
|
||||
HorizontalAlignment="Center"
|
||||
DockPanel.Dock="Bottom"
|
||||
Orientation="Horizontal"
|
||||
Spacing="16">
|
||||
<RadioButton Content="像素(Pixel)" GroupName="XAxis" />
|
||||
<RadioButton
|
||||
Content="波长(nm)"
|
||||
GroupName="XAxis"
|
||||
IsChecked="True" />
|
||||
<RadioButton Content="波数(cm⁻¹)" GroupName="XAxis" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Chart -->
|
||||
<sp:AvaPlot x:Name="AvaPlot" />
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using RamanApp.ViewModels;
|
||||
using ScottPlot;
|
||||
|
||||
namespace RamanApp.Views;
|
||||
|
||||
public partial class SpectrumChartView : UserControl
|
||||
{
|
||||
public SpectrumChartView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (DataContext is SpectrumChartViewModel vm)
|
||||
{
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
RefreshChart(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is SpectrumChartViewModel vm && e.PropertyName is nameof(SpectrumChartViewModel.YData) or nameof(SpectrumChartViewModel.XData))
|
||||
{
|
||||
RefreshChart(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshChart(SpectrumChartViewModel vm)
|
||||
{
|
||||
var plot = AvaPlot.Plot;
|
||||
plot.Clear();
|
||||
|
||||
if (vm.XData.Length > 0 && vm.YData.Length > 0)
|
||||
{
|
||||
var scatter = plot.Add.Scatter(vm.XData, vm.YData);
|
||||
scatter.LineWidth = 1;
|
||||
scatter.MarkerSize = 0;
|
||||
scatter.Color = ScottPlot.Color.FromHex("#1E90FF");
|
||||
}
|
||||
|
||||
plot.Axes.Bottom.Label.Text = vm.XAxisLabel;
|
||||
plot.Axes.Left.Label.Text = vm.YAxisLabel;
|
||||
plot.Axes.AutoScale();
|
||||
AvaPlot.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="RamanApp.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -197,6 +197,8 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
|
||||
return SpectrometerResponse.Ok();
|
||||
}
|
||||
|
||||
private const int MaxIntegrationTimeMicroseconds = 5_000_000;
|
||||
|
||||
private async Task<SpectrometerResponse> ConfigureAcquisitionAsync(SpectrometerRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.AcquisitionSettings is null)
|
||||
@@ -204,6 +206,13 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
|
||||
return SpectrometerResponse.Fail(SpectrometerErrorCodes.InvalidValue, "ConfigureAcquisition requires acquisition settings.");
|
||||
}
|
||||
|
||||
if (request.AcquisitionSettings.IntegrationTimeMicroseconds > MaxIntegrationTimeMicroseconds)
|
||||
{
|
||||
return SpectrometerResponse.Fail(
|
||||
SpectrometerErrorCodes.InvalidValue,
|
||||
$"Integration time {request.AcquisitionSettings.IntegrationTimeMicroseconds} us exceeds maximum allowed {MaxIntegrationTimeMicroseconds} us (5s).");
|
||||
}
|
||||
|
||||
var device = FindConnectedDevice(request.Target);
|
||||
if (device is null)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class SpectrometerCommandSessionTests
|
||||
|
||||
Assert.Contains(entries, entry => entry.Message == "Usage:" && !entry.ShowPrefix);
|
||||
var acquire = Assert.Single(entries, entry => entry.Message.Contains("spec acquire <device>"));
|
||||
Assert.Contains(entries, entry => entry.Message.Contains("--integration is in microseconds/us"));
|
||||
Assert.False(acquire.ShowPrefix);
|
||||
Assert.NotNull(acquire.Segments);
|
||||
Assert.Equal(ConsoleColor.Green, acquire.Segments[0].Color);
|
||||
|
||||
Reference in New Issue
Block a user