update:多人跳绳点位配置

This commit is contained in:
ltx 2026-01-21 17:51:29 +08:00
parent 3e8df48443
commit ad713cfe8f
5 changed files with 1204 additions and 17 deletions

View File

@ -59,5 +59,14 @@ namespace AiSportsMicrospaceDB.Entities
[Comment("端口")]
[Column(TypeName = "nvarchar(20)")]
public string Port { get; set; }
/// <summary>
///端口
/// </summary>
[Display(Name = "运动角宽度")]
[Comment("运动角宽度")]
[Column(TypeName = "int")]
public int HomeWidth { get; set; }
}
}

View File

@ -110,7 +110,7 @@
<!-- 密码弹窗 -->
<Grid x:Name="SetDialogGrid" Visibility="Hidden" Background="#80000000">
<Border CornerRadius="10" Width="420" Height="330" Background="#FFF">
<Border CornerRadius="10" Width="420" Height="530" Background="#FFF">
<Grid HorizontalAlignment="Center" hc:BorderElement.CornerRadius="10" VerticalAlignment="Top" Width="420" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@ -124,28 +124,36 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="基础配置" FontSize="18" Margin="12" FontWeight="Bold" Grid.Row="0"/>
<TextBlock Text="*重新配置摄像头后请ping测试通过再保存" FontSize="10" Margin="30,34,10,5" Foreground="red" Grid.RowSpan="1"/>
<TextBlock Text="摄像头配置" FontSize="18" Margin="12,20,0,0" FontWeight="Bold" Grid.Row="0"/>
<TextBlock Text="*重新配置摄像头后请ping测试通过再保存" FontSize="10" Margin="30,54,10,5" Foreground="red" Grid.RowSpan="1"/>
<hc:TextBox hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="摄像头IP:" Width="400" Text="172.17.30.64" Grid.Row="2" x:Name="host" />
<hc:TextBox hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="账号:" Width="400" Text="admin" Grid.Row="3" Margin="0,15" x:Name="name"/>
<hc:TextBox hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="密码:" Width="400" Text="yd708090" Grid.Row="4" x:Name="password" />
<StackPanel Grid.Row="7" Orientation="Horizontal" HorizontalAlignment="Right" Margin="12">
<StackPanel Grid.Row="9" Orientation="Horizontal" HorizontalAlignment="Right" Margin="12,20,0,0">
<RadioButton Margin="0,0,220,0" Style="{DynamicResource RadioButtonSameAsButtonPrimary}" Content="Ping" Click="Ping_Click" />
<RadioButton Style="{DynamicResource RadioGroupItemSingle}" Content="取消" x:Name="SetCancelButton" Click="SetCancelButton_Click" />
<RadioButton Margin="10,0" Style="{StaticResource RadioButtonSameAsButtonPrimary}" Content="保存" Foreground="#fff" x:Name="SetOkButton1" Click="SetOkButton_Click"/>
<RadioButton Margin="10,0,20,0" Style="{StaticResource RadioButtonSameAsButtonPrimary}" Content="保存" Foreground="#fff" x:Name="SetOkButton1" Click="SetOkButton_Click"/>
</StackPanel>
<TextBlock Text="基础配置" FontSize="18" Margin="12,30,0,0" FontWeight="Bold" Grid.Row="7"/>
<hc:TextBox hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="运动角宽度(米):" Width="400" Text="5" Grid.Row="8" x:Name="homeWidth" />
<TextBlock Text="其他配置" FontSize="18" Margin="12,30,0,0" FontWeight="Bold" Grid.Row="8"/>
<StackPanel Grid.Row="9" Orientation="Horizontal" HorizontalAlignment="Center" Margin="12">
<TextBlock Text="其他配置" FontSize="18" Margin="12,40,0,0" FontWeight="Bold" Grid.Row="10"/>
<StackPanel Grid.Row="11" Orientation="Horizontal" HorizontalAlignment="Center" Margin="12">
<RadioButton Style="{DynamicResource RadioButtonSameAsButtonPrimary}" Foreground="#fff" Content="多人跳绳" Click="GoRopeSet" />
<RadioButton Margin="30,0,0,0" Style="{StaticResource RadioButtonSameAsButtonPrimary}" Foreground="#fff" Content="立定跳远" Click="GoLongSet"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@ -89,7 +89,7 @@ namespace Wpf_AiSportsMicrospace
coverFlow.SelectedIndex = 0;
Loaded += Window_Loaded;
Unloaded += Home_Unloaded;
Load_Base();
Utils.PlayBackgroundMusic("homeprojectselected.mp3", true);
}
@ -105,7 +105,6 @@ namespace Wpf_AiSportsMicrospace
_mainWin.WebcamClient.StartExtract();
Utils.PreloadGifs();
Load_Base();
}
private void Home_Unloaded(object sender, RoutedEventArgs e)
{
@ -291,7 +290,7 @@ namespace Wpf_AiSportsMicrospace
var baseData = await db.BasicConfig.FirstOrDefaultAsync(x => x.Code == AppSettings.Code);
if (baseData == null)
{
baseData = new BasicConfig { Code = AppSettings.Code, Host = host.Text, UserName = name.Text, Password = password.Text };
baseData = new BasicConfig { Code = AppSettings.Code, Host = host.Text, UserName = name.Text, Password = password.Text , HomeWidth = int.Parse(homeWidth.Text)};
db.BasicConfig.Add(baseData);
db.SaveChanges();
}
@ -300,6 +299,7 @@ namespace Wpf_AiSportsMicrospace
host.Text = baseData.Host;
name.Text = baseData.UserName;
password.Text = baseData.Password;
homeWidth.Text = baseData.HomeWidth.ToString();
}
}
@ -317,7 +317,8 @@ namespace Wpf_AiSportsMicrospace
Code = AppSettings.Code,
Host = host.Text,
UserName = name.Text,
Password = password.Text
Password = password.Text,
HomeWidth = int.Parse(homeWidth.Text)
});
}
else
@ -325,6 +326,7 @@ namespace Wpf_AiSportsMicrospace
baseData.Host = host.Text;
baseData.UserName = name.Text;
baseData.Password = password.Text;
baseData.HomeWidth = int.Parse(homeWidth.Text);
db.BasicConfig.Update(baseData);
}
db.SaveChanges();
@ -336,11 +338,14 @@ namespace Wpf_AiSportsMicrospace
//多人跳绳
private void GoRopeSet(object sender, RoutedEventArgs e)
{
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var mainWindow = new MainWindow(db);
mainWindow.Show();
//var app = (App)Application.Current;
//using var scope = App.AppHost.Services.CreateScope();
//var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
//var mainWindow = new MainWindow(db);
//mainWindow.Show();
var mainWin = Application.Current.MainWindow as Main;
mainWin.SwitchPageWithMaskAnimation(new MultiJumpRope(), true);
}
//跳远

View File

@ -0,0 +1,170 @@
<UserControl x:Class="Wpf_AiSportsMicrospace.Views.MultiJumpRope"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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"
Loaded="Window_Loaded"
mc:Ignorable="d" Width="1920" Height="1080">
<UserControl.Resources>
<DropShadowEffect x:Key="DropShadowEffect"
Color="#22000000"
BlurRadius="12"
ShadowDepth="2"
Opacity="0.5"/>
</UserControl.Resources>
<Grid>
<!-- 视频图像层 -->
<Image x:Name="videoImage" Stretch="Uniform" />
<!-- 主画布层 -->
<Canvas x:Name="MainCanvas" >
<!-- 背景网格 -->
<!--<Canvas.Background>
<DrawingBrush Viewport="0,0,50,50" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,50,50"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="0.5"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<GeometryGroup>
<LineGeometry StartPoint="0,0" EndPoint="0,50"/>
<LineGeometry StartPoint="0,0" EndPoint="50,0"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Canvas.Background>-->
<!-- 动态生成的点位容器 -->
<ItemsControl x:Name="PointsContainer">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!-- 连接线 -->
<Polyline x:Name="ConnectionLine" Stroke="#6366F1"
StrokeDashArray="5,5" Opacity="0.6"/>
<!-- 坐标显示标签 -->
<hc:Tag ShowCloseButton="False" IsSelected="True" x:Name="CoordinateDisplay"
Content="位置点" Padding="18,0" FontSize="14" Visibility="Visible"/>
</Canvas>
<!-- 返回首页按钮 -->
<Button Content="返回首页" Click="Label_MouseDown"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="20" Width="100" Height="30"/>
<!-- 控制面板 -->
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Margin="20" Width="280" Opacity="0.8">
<Border Background="#FFFFFF" Padding="15" CornerRadius="8" Effect="{StaticResource DropShadowEffect}">
<StackPanel>
<!-- 标题 -->
<TextBlock Text="多人跳绳点配置" FontSize="18" FontWeight="Bold"
Margin="0,0,0,15" HorizontalAlignment="Center"/>
<!-- 分隔线 -->
<Separator Margin="0,0,0,10"/>
<!-- HomeWidth 显示 -->
<!--<TextBlock Text="场地宽度 (HomeWidth):" FontSize="14" Margin="0,5,0,5"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,5">
<TextBlock x:Name="HomeWidthText" Text="6.0" FontSize="24"
FontWeight="Bold" Foreground="#3B82F6"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBlock Text="米" FontSize="14" VerticalAlignment="Center"
Foreground="#6B7280"/>
</StackPanel>-->
<!-- 点位数量显示 -->
<Border Background="#DBEAFE" CornerRadius="4" Margin="0,10,0,10">
<TextBlock x:Name="PointCountText" Text="点位数量: 6 个"
FontSize="16" FontWeight="Bold" Foreground="#1E40AF"
HorizontalAlignment="Center" Padding="10,8"/>
</Border>
<!-- 操作说明 -->
<TextBlock Text="💡 点击点位后可在 Y 轴方向拖动调整"
FontSize="12" Foreground="#6B7280" TextWrapping="Wrap"
Margin="0,10,0,10"/>
<!-- 分隔线 -->
<Separator Margin="0,5,0,10"/>
<!-- 功能按钮 -->
<Button x:Name="LoadPointsButton" Content="📥 加载点数据"
Click="LoadPointsButton_Click"
Margin="0,5" Height="35" Background="#10B981" Foreground="White"
FontSize="14" FontWeight="Bold"/>
<Button x:Name="SavePointsButton" Content="💾 保存点数据"
Click="SavePointsButton_Click"
Margin="0,5" Height="35" Background="#3B82F6" Foreground="White"
FontSize="14" FontWeight="Bold"/>
<Button x:Name="ResetPointsButton" Content="🔄 重置为默认"
Click="ResetPointsButton_Click"
Margin="0,5" Height="35" Background="#EF4444" Foreground="White"
FontSize="14" FontWeight="Bold"/>
<!-- 状态显示 -->
<TextBlock x:Name="StatusText" Text="就绪"
Margin="0,15,0,0" TextWrapping="Wrap"
FontSize="12" Foreground="#6B7280"/>
</StackPanel>
</Border>
</StackPanel>
<!-- 右侧点位数据面板 -->
<Border HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="20,20,320,20" Width="220" Background="White" Opacity="0.8"
CornerRadius="8" Effect="{StaticResource DropShadowEffect}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="15">
<TextBlock Text="点位坐标数据" FontSize="16" FontWeight="Bold"
Margin="0,0,0,10" HorizontalAlignment="Center"/>
<Separator Margin="0,0,0,10"/>
<ItemsControl x:Name="PointsDataList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{Binding BackgroundColor}"
CornerRadius="4" Margin="0,1">
<StackPanel Orientation="Horizontal"
Margin="10,8" VerticalAlignment="Center">
<Ellipse Width="24" Height="24"
Fill="#34D399" Margin="0,0,10,0"/>
<TextBlock Text="{Binding PointLabel}"
Foreground="#000"
FontWeight="Bold"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Width="24" TextAlignment="Center"/>
<TextBlock Text="{Binding CoordinateText}"
FontFamily="Consolas"
FontSize="13"
VerticalAlignment="Center"
Margin="10,0,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,995 @@
using AiSportsMicrospaceDB.DBContext;
using AiSportsMicrospaceDB.Entities;
using Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Yztob.AiSports.Sensors.Abstractions;
using Yztob.AiSports.Sensors.Things;
using Line = System.Windows.Shapes.Line;
using Point = System.Windows.Point;
namespace Wpf_AiSportsMicrospace.Views
{
/// <summary>
/// 多人大绳点位配置页面
/// 功能:
/// 1. 根据 HomeWidth 动态生成 5-7 个点位
/// 2. 支持 Y 轴拖动调整
/// 3. 从数据库加载/保存点位数据
/// </summary>
public partial class MultiJumpRope : UserControl
{
#region 私有变量
private bool _isDragging = false;
private Ellipse _draggedEllipse;
private double _dragStartY;
private double _originalY;
/// <summary>
/// 点位数据列表
/// </summary>
private List<JumpRopePoint> _points = new List<JumpRopePoint>();
/// <summary>
/// 当前场地宽度
/// </summary>
private double _homeWidth = 6.0;
/// <summary>
/// 点位半径(长轴)
/// </summary>
private const double PointRadiusY = 35;
/// <summary>
/// 点位短轴(扁平化比例 0.5)
/// </summary>
private const double PointRadiusX = 70;
private int _lastFrameNumber = -1;
private WriteableBitmap _videoBitmap;
#endregion
#region 构造函数
public MultiJumpRope()
{
InitializeComponent();
SetupDragEvents();
}
#endregion
#region 私有方法
/// <summary>
/// 页面加载完成事件
/// </summary>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadPointsFromDatabase();
}
/// <summary>
/// 设置拖拽事件
/// </summary>
private void SetupDragEvents()
{
MainCanvas.MouseMove += MainCanvas_MouseMove;
MainCanvas.MouseLeftButtonUp += MainCanvas_MouseLeftButtonUp;
MainCanvas.MouseLeave += MainCanvas_MouseLeave;
}
/// <summary>
/// 根据 HomeWidth 计算点位数量
/// HomeWidth <= 5:5 个点位
/// 5 < HomeWidth < 7:6 个点位
/// HomeWidth >= 7:7 个点位
/// </summary>
private int CalculatePointCount(double homeWidth)
{
if (homeWidth <= 5)
return 5;
else if (homeWidth > 5 && homeWidth < 7)
return 6;
else
return 7;
}
/// <summary>
/// 从数据库加载点位数据
/// </summary>
private async void LoadPointsFromDatabase()
{
try
{
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 获取场地宽度配置
var baseConfig = await db.BasicConfig.FirstOrDefaultAsync(x => x.Code == AppSettings.Code);
if (baseConfig == null)
{
UpdateStatus("未找到相机主机配置");
return;
}
_homeWidth = baseConfig.HomeWidth;
int pointCount = CalculatePointCount(_homeWidth);
// 更新 UI 显示
//HomeWidthText.Text = _homeWidth.ToString("F1");
PointCountText.Text = $"点位数量: {pointCount} 个";
// 清空现有控件
ClearPoints();
// 获取已保存的点位数据
var pointsData = await db.SixGroupPoints.FirstOrDefaultAsync(x => x.Code == AppSettings.Code);
if (pointsData != null)
{
// 从数据库加载保存的点位
LoadPointsFromDatabaseData(pointsData, pointCount);
}
else
{
// 生成默认点位位置
CreateDefaultPoints(pointCount);
}
var BaseData = await db.BasicConfig.FirstOrDefaultAsync(x => x.Code == AppSettings.Code);
if (BaseData == null)
{
this.ShowError("相机主机地址/IP不能为空。");
return;
}
var _webcamClient = WebcamClient.CreateRTSP(BaseData.Host, BaseData.UserName, BaseData.Password);
//处理抽帧回调
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
_webcamClient.StartExtract();//开始抽帧
UpdateStatus($"点数据加载成功 - 共 {pointCount} 个点位");
}
catch (Exception ex)
{
UpdateStatus($"加载点数据失败: {ex.Message}");
}
}
private async void OnFrameExtracted(VideoFrame frame)
{
if (frame == null) return;
// 跳帧:每秒只渲染 10~15 帧,降低 CPU 占用
if (_lastFrameNumber != -1 && frame.Number - _lastFrameNumber < 2) return;
_lastFrameNumber = (int)frame.Number;
//获得帧二进制流,保存图像、人体识别竺
var buffer = frame.GetImageBuffer(Yztob.AiSports.Sensors.Things.ImageFormat.Jpeg).ToArray();
//await File.WriteAllBytesAsync($"./temps/{frame.Number}.jpg", buffer);
// === 显示到 WPF ===
BitmapImage bitmap = null;
await Task.Run(() =>
{
using var ms = new MemoryStream(buffer);
bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = ms;
bitmap.EndInit();
bitmap.Freeze(); // 跨线程安全
});
// UI 线程显示
Application.Current.Dispatcher.Invoke(() =>
{
if (videoImage == null) return;
if (_videoBitmap == null ||
_videoBitmap.PixelWidth != bitmap.PixelWidth ||
_videoBitmap.PixelHeight != bitmap.PixelHeight)
{
_videoBitmap = new WriteableBitmap(bitmap.PixelWidth, bitmap.PixelHeight,
96, 96, PixelFormats.Bgra32, null);
videoImage.Source = _videoBitmap;
}
_videoBitmap.Lock();
bitmap.CopyPixels(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight),
_videoBitmap.BackBuffer,
_videoBitmap.BackBufferStride * bitmap.PixelHeight,
_videoBitmap.BackBufferStride);
_videoBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
_videoBitmap.Unlock();
});
}
/// <summary>
/// 清空所有点位控件
/// </summary>
private void ClearPoints()
{
_points.Clear();
PointsContainer.ItemsSource = null;
ConnectionLine.Points.Clear();
// 清除画布上除了背景和固定控件外的所有元素
var elementsToRemove = new List<UIElement>();
foreach (var child in MainCanvas.Children)
{
if (child is Ellipse || child is Border || child is Line)
{
var element = child as FrameworkElement;
if (element?.Name != "ConnectionLine" &&
element?.Name != "PointsContainer" &&
element?.Name != "CoordinateDisplay")
{
elementsToRemove.Add(element);
}
}
}
foreach (var element in elementsToRemove)
{
MainCanvas.Children.Remove(element);
}
}
/// <summary>
/// 从数据库数据加载点位
/// db.SixGroupPoints 包含 Point1_X, Point1_Y, Point2_X, Point2_Y 等字段
/// </summary>
private void LoadPointsFromDatabaseData(SixGroupPoints data, int pointCount)
{
for (int i = 1; i <= pointCount; i++)
{
var point = new JumpRopePoint
{
Id = i,
Index = i
};
// 从数据库读取 Point 类型的属性
var pointProperty = typeof(SixGroupPoints).GetProperty($"Point{i}");
if (pointProperty != null)
{
var dbPoint = pointProperty.GetValue(data);
if (dbPoint != null)
{
// 获取 Point 的 X 和 Y 属性
var xProp = dbPoint.GetType().GetProperty("X");
var yProp = dbPoint.GetType().GetProperty("Y");
if (xProp != null && yProp != null)
{
point.X = (double)(xProp.GetValue(dbPoint) ?? 0) * 1080;
point.Y = (double)(yProp.GetValue(dbPoint) ?? 0) * 1920;
}
}
}
// 如果坐标为 0,则使用默认值
if (point.X == 0) point.X = GetDefaultX(i, pointCount);
if (point.Y == 0) point.Y = GetDefaultY();
point.BackgroundColor = i % 2 == 0 ? "#F3F4F6" : "#FFFFFF";
_points.Add(point);
CreatePointControl(point);
}
RefreshPointsDataList();
UpdateConnectionLine();
}
/// <summary>
/// 创建默认点位
/// </summary>
private void CreateDefaultPoints(int pointCount)
{
for (int i = 1; i <= pointCount; i++)
{
var point = new JumpRopePoint
{
Id = i,
Index = i,
X = GetDefaultX(i, pointCount),
Y = GetDefaultY(),
BackgroundColor = i % 2 == 0 ? "#F3F4F6" : "#FFFFFF"
};
_points.Add(point);
CreatePointControl(point);
}
RefreshPointsDataList();
UpdateConnectionLine();
}
/// <summary>
/// 获取默认 X 坐标(均匀分布)
/// </summary>
private double GetDefaultX(int index, int pointCount)
{
double canvasWidth = MainCanvas.ActualWidth > 0 ? MainCanvas.ActualWidth : 1920;
// 小边距,让第一个点和最后一个点靠近边缘
double margin = 90;
double availableWidth = canvasWidth - (margin * 2);
// 均匀分布在 margin 和 canvasWidth - margin 之间
if (pointCount > 1)
{
double spacing = availableWidth / (pointCount - 1);
return margin + (spacing * (index - 1));
}
return canvasWidth / 2;
}
/// <summary>
/// 获取默认 Y 坐标(画布中间)
/// </summary>
private double GetDefaultY()
{
double canvasHeight = MainCanvas.ActualHeight > 0 ? MainCanvas.ActualHeight : 800;
return canvasHeight / 2;
}
/// <summary>
/// 创建点位控件
/// </summary>
private void CreatePointControl(JumpRopePoint point)
{
// 创建外圈光晕
var glow = new Ellipse
{
Width = (PointRadiusX + 10) * 2,
Height = (PointRadiusY + 10) * 2,
Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3434D399")),
Opacity = 0.5
};
Canvas.SetLeft(glow, point.X - glow.Width / 2);
Canvas.SetTop(glow, point.Y - glow.Height / 2);
Canvas.SetZIndex(glow, 1);
// 创建主椭圆
var ellipse = new Ellipse
{
Name = $"PointEllipse_{point.Id}",
Width = PointRadiusX * 2,
Height = PointRadiusY * 2,
Fill = new LinearGradientBrush
{
StartPoint = new Point(0.5, 0),
EndPoint = new Point(0.5, 1),
GradientStops = new GradientStopCollection
{
new GradientStop((Color)ColorConverter.ConvertFromString("#34D399"), 0),
new GradientStop((Color)ColorConverter.ConvertFromString("#059669"), 1)
}
},
Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#059669")),
StrokeThickness = 2,
Cursor = Cursors.SizeNS // 垂直方向光标
};
// 绑定数据
ellipse.DataContext = point;
ellipse.Tag = point;
// 设置位置
Canvas.SetLeft(ellipse, point.X - PointRadiusX);
Canvas.SetTop(ellipse, point.Y - PointRadiusY);
Canvas.SetZIndex(ellipse, 10);
// 添加鼠标事件
ellipse.MouseLeftButtonDown += Point_MouseLeftButtonDown;
ellipse.MouseMove += Point_MouseMove; // 新增
ellipse.MouseLeftButtonUp += Point_MouseLeftButtonUp; // 新增
ellipse.MouseEnter += Point_MouseEnter;
ellipse.MouseLeave += Point_MouseLeave;
// 创建标签
var label = new TextBlock
{
Text = $"P{point.Id}",
Foreground = Brushes.White,
FontSize = 16,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var labelBorder = new Border
{
Child = label,
Width = PointRadiusX * 2,
Height = PointRadiusY * 2,
Background = Brushes.Transparent,
IsHitTestVisible = false // 让鼠标事件穿透到下层的椭圆
};
Canvas.SetLeft(labelBorder, point.X - PointRadiusX);
Canvas.SetTop(labelBorder, point.Y - PointRadiusY);
Canvas.SetZIndex(labelBorder, 11);
// 创建 X 轴引导线(虚线)
var guideLine = new Line
{
X1 = point.X,
Y1 = 0,
X2 = point.X,
Y2 = MainCanvas.ActualHeight > 0 ? MainCanvas.ActualHeight : 800,
Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#fff")),
StrokeThickness = 2,
StrokeDashArray = new DoubleCollection { 5, 5 },
Opacity = 0.8
};
Canvas.SetZIndex(guideLine, 2);
// 添加到画布
MainCanvas.Children.Add(glow);
MainCanvas.Children.Add(guideLine);
MainCanvas.Children.Add(ellipse);
MainCanvas.Children.Add(labelBorder);
}
/// <summary>
/// 更新右侧点位数据列表
/// </summary>
private void RefreshPointsDataList()
{
var items = _points.Select((point, index) => new
{
PointLabel = point.Id.ToString(),
CoordinateText = $"X:{Math.Round(point.X)} Y:{Math.Round(point.Y)}",
BackgroundColor = point.BackgroundColor
}).ToList();
PointsDataList.ItemsSource = items;
}
/// <summary>
/// 更新连接线
/// </summary>
private void UpdateConnectionLine()
{
var points = new PointCollection();
foreach (var point in _points.OrderBy(p => p.X))
{
points.Add(new Point(point.X, point.Y));
}
ConnectionLine.Points = points;
}
#endregion
#region 鼠标事件处理
/// <summary>
/// 点位鼠标按下事件
/// </summary>
private void Point_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDragging = true;
_draggedEllipse = sender as Ellipse;
_dragStartY = e.GetPosition(MainCanvas).Y;
if (_draggedEllipse.Tag is JumpRopePoint point)
{
_originalY = point.Y;
}
_draggedEllipse.CaptureMouse();
// 提升 Z 索引
Canvas.SetZIndex(_draggedEllipse, 100);
// 改变颜色表示选中状态
_draggedEllipse.Fill = new LinearGradientBrush
{
StartPoint = new Point(0.5, 0),
EndPoint = new Point(0.5, 1),
GradientStops = new GradientStopCollection
{
new GradientStop((Color)ColorConverter.ConvertFromString("#FBBF24"), 0),
new GradientStop((Color)ColorConverter.ConvertFromString("#D97706"), 1)
}
};
_draggedEllipse.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D97706"));
UpdateStatus($"正在调整 P{(_draggedEllipse.Tag as JumpRopePoint)?.Id} 点的 Y 坐标");
}
/// <summary>
/// 点位鼠标移动事件(在椭圆上直接处理)
/// </summary>
private void Point_MouseMove(object sender, MouseEventArgs e)
{
if (_isDragging && _draggedEllipse != null)
{
var currentY = e.GetPosition(MainCanvas).Y;
var deltaY = currentY - _dragStartY;
if (_draggedEllipse.Tag is JumpRopePoint point)
{
// 计算新位置(只允许 Y 轴移动)
double newY = _originalY + deltaY;
// 限制在画布范围内
double canvasHeight = MainCanvas.ActualHeight > 0 ? MainCanvas.ActualHeight : 1080;
newY = Math.Max(PointRadiusY, Math.Min(canvasHeight - PointRadiusY, newY));
// 更新点位数据
point.Y = newY;
// 更新椭圆位置
Canvas.SetTop(_draggedEllipse, newY - PointRadiusY);
// 更新光晕位置
UpdateGlowPosition(point);
// 更新标签位置
UpdateLabelPosition(point);
// 更新引导线
UpdateGuideLine(point, canvasHeight);
// 更新连接线
UpdateConnectionLine();
// 更新坐标显示
UpdateCoordinateDisplay(new Point(point.X, newY), $"P{point.Id}");
// 更新右侧数据列表
RefreshPointsDataList();
// 重置拖动起始位置
_dragStartY = currentY;
_originalY = newY;
}
}
}
/// <summary>
/// 点位鼠标释放事件(在椭圆上直接处理)
/// </summary>
private void Point_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isDragging)
{
_isDragging = false;
if (_draggedEllipse != null)
{
_draggedEllipse.ReleaseMouseCapture();
Canvas.SetZIndex(_draggedEllipse, 10);
// 恢复颜色
_draggedEllipse.Fill = new LinearGradientBrush
{
StartPoint = new Point(0.5, 0),
EndPoint = new Point(0.5, 1),
GradientStops = new GradientStopCollection
{
new GradientStop((Color)ColorConverter.ConvertFromString("#34D399"), 0),
new GradientStop((Color)ColorConverter.ConvertFromString("#059669"), 1)
}
};
_draggedEllipse.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#059669"));
if (_draggedEllipse.Tag is JumpRopePoint point)
{
UpdateStatus($"P{point.Id} 点位置已调整为 Y = {Math.Round(point.Y)}");
}
_draggedEllipse = null;
}
// 隐藏坐标显示
CoordinateDisplay.Visibility = Visibility.Collapsed;
}
}
/// <summary>
/// 更新光晕位置
/// </summary>
private void UpdateGlowPosition(JumpRopePoint point)
{
var ellipse = MainCanvas.Children
.OfType<Ellipse>()
.FirstOrDefault(e => e.Name == $"PointEllipse_{point.Id}");
if (ellipse != null)
{
// 光晕尺寸
double glowWidth = (PointRadiusX + 10) * 2;
double glowHeight = (PointRadiusY + 10) * 2;
double expectedLeft = point.X - glowWidth / 2;
// 根据尺寸和位置查找光晕
var glow = MainCanvas.Children
.OfType<Ellipse>()
.FirstOrDefault(e =>
{
var left = Canvas.GetLeft(e);
return e.Width == glowWidth &&
e.Height == glowHeight &&
Math.Abs(left - expectedLeft) < 10;
});
if (glow != null)
{
Canvas.SetTop(glow, point.Y - glow.Height / 2);
}
}
}
/// <summary>
/// 更新标签位置
/// </summary>
private void UpdateLabelPosition(JumpRopePoint point)
{
var ellipse = MainCanvas.Children
.OfType<Ellipse>()
.FirstOrDefault(e => e.Name == $"PointEllipse_{point.Id}");
if (ellipse != null)
{
var labelBorder = MainCanvas.Children
.OfType<Border>()
.FirstOrDefault(b =>
{
var parent = b.Parent as Canvas;
if (parent != null)
{
return Canvas.GetLeft(b) == Canvas.GetLeft(ellipse) &&
Canvas.GetTop(b) == Canvas.GetTop(ellipse);
}
return false;
});
if (labelBorder != null)
{
Canvas.SetTop(labelBorder, point.Y - PointRadiusY);
}
}
}
/// <summary>
/// 更新引导线
/// </summary>
private void UpdateGuideLine(JumpRopePoint point, double canvasHeight)
{
var guideLine = MainCanvas.Children
.OfType<Line>()
.FirstOrDefault(l => Math.Abs(l.X1 - point.X) < 5);
if (guideLine != null)
{
guideLine.Y1 = 0;
guideLine.Y2 = canvasHeight;
}
}
/// <summary>
/// 画布鼠标移动事件
/// </summary>
private void MainCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (_isDragging && _draggedEllipse != null)
{
var currentY = e.GetPosition(MainCanvas).Y;
var deltaY = currentY - _dragStartY;
if (_draggedEllipse.Tag is JumpRopePoint point)
{
// 计算新位置(只允许 Y 轴移动)
double newY = _originalY + deltaY;
// 限制在画布范围内
double canvasHeight = MainCanvas.ActualHeight > 0 ? MainCanvas.ActualHeight : 800;
newY = Math.Max(PointRadiusY, Math.Min(canvasHeight - PointRadiusY, newY));
// 更新点位数据
point.Y = newY;
// 更新椭圆位置
Canvas.SetTop(_draggedEllipse, newY - PointRadiusY);
// 更新连接线
UpdateConnectionLine();
// 更新坐标显示
UpdateCoordinateDisplay(new Point(point.X, newY), $"P{point.Id}");
// 更新右侧数据列表
RefreshPointsDataList();
// 重置拖动起始位置
_dragStartY = currentY;
_originalY = newY;
}
}
}
/// <summary>
/// 画布鼠标释放事件
/// </summary>
private void MainCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isDragging)
{
_isDragging = false;
if (_draggedEllipse != null)
{
_draggedEllipse.ReleaseMouseCapture();
Canvas.SetZIndex(_draggedEllipse, 10);
// 恢复颜色
_draggedEllipse.Fill = new LinearGradientBrush
{
StartPoint = new Point(0.5, 0),
EndPoint = new Point(0.5, 1),
GradientStops = new GradientStopCollection
{
new GradientStop((Color)ColorConverter.ConvertFromString("#34D399"), 0),
new GradientStop((Color)ColorConverter.ConvertFromString("#059669"), 1)
}
};
_draggedEllipse.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#059669"));
if (_draggedEllipse.Tag is JumpRopePoint point)
{
UpdateStatus($"P{point.Id} 点位置已调整为 Y = {Math.Round(point.Y)}");
}
_draggedEllipse = null;
}
// 隐藏坐标显示
CoordinateDisplay.Visibility = Visibility.Collapsed;
}
}
/// <summary>
/// 画布鼠标离开事件
/// </summary>
private void MainCanvas_MouseLeave(object sender, MouseEventArgs e)
{
if (_isDragging)
{
MainCanvas_MouseLeftButtonUp(sender, e);
}
}
private void MainCanvas_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
throw new NotImplementedException();
}
/// <summary>
/// 鼠标进入点位
/// </summary>
private void Point_MouseEnter(object sender, MouseEventArgs e)
{
var ellipse = sender as Ellipse;
ellipse.Opacity = 0.9;
// 显示坐标提示
if (ellipse.Tag is JumpRopePoint point)
{
var position = new Point(point.X, point.Y);
UpdateCoordinateDisplay(position, $"P{point.Id}");
}
}
/// <summary>
/// 鼠标离开点位
/// </summary>
private void Point_MouseLeave(object sender, MouseEventArgs e)
{
var ellipse = sender as Ellipse;
ellipse.Opacity = 1.0;
if (!_isDragging)
{
CoordinateDisplay.Visibility = Visibility.Collapsed;
}
}
/// <summary>
/// 更新坐标显示
/// </summary>
private void UpdateCoordinateDisplay(System.Windows.Point position, string pointName)
{
CoordinateDisplay.Content = $"{pointName}: ({position.X:F0}, {position.Y:F0})";
CoordinateDisplay.Margin = new Thickness(position.X + 15, position.Y - 80, 0, 0);
CoordinateDisplay.Visibility = Visibility.Visible;
}
#endregion
#region 按钮事件处理
/// <summary>
/// 加载点数据按钮点击事件
/// </summary>
private void LoadPointsButton_Click(object sender, RoutedEventArgs e)
{
LoadPointsFromDatabase();
}
/// <summary>
/// 保存点数据按钮点击事件
/// </summary>
private void SavePointsButton_Click(object sender, RoutedEventArgs e)
{
SavePointsToDatabase();
}
/// <summary>
/// 重置点数据按钮点击事件
/// </summary>
private void ResetPointsButton_Click(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show(
"确定要将所有点位重置为默认位置吗?",
"确认重置",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
ResetToDefault();
}
}
/// <summary>
/// 返回首页按钮点击事件
/// </summary>
private void Label_MouseDown(object sender, RoutedEventArgs e)
{
var mainWin = Application.Current.MainWindow as Main;
var newPage = new Home();
mainWin?.SwitchPageWithMaskAnimation(newPage, true);
}
#endregion
#region 保存和重置方法
/// <summary>
/// 保存点位数据到数据库
/// </summary>
private void SavePointsToDatabase()
{
try
{
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pointsData = db.SixGroupPoints.FirstOrDefault(x => x.Code == AppSettings.Code);
if (pointsData == null)
{
pointsData = new SixGroupPoints { Code = AppSettings.Code };
db.SixGroupPoints.Add(pointsData);
}
// 获取 Point 类型(支持 System.Drawing.Point 或 System.Windows.Point)
var pointType = Type.GetType("System.Windows.Point, WindowsBase");
if (pointType != null)
{
foreach (var point in _points)
{
var pointProperty = typeof(SixGroupPoints).GetProperty($"Point{point.Id}");
if (pointProperty != null)
{
// 创建 Point 实例并保存
var pointInstance = Activator.CreateInstance(pointType, point.X / 1080, point.Y / 1920);
var pointNew = new AiSportsMicrospaceDB.Entities.Point {
X =point.X / 1080,Y= point.Y / 1920
};
pointProperty.SetValue(pointsData, pointNew);
}
}
}
db.SaveChanges();
UpdateStatus("点数据保存成功");
}
catch (Exception ex)
{
UpdateStatus($"保存点数据失败: {ex.Message}");
}
}
/// <summary>
/// 重置为默认位置
/// </summary>
private void ResetToDefault()
{
try
{
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 删除现有的点位数据
var existingData = db.SixGroupPoints.FirstOrDefault(x => x.Code == AppSettings.Code);
if (existingData != null)
{
db.SixGroupPoints.Remove(existingData);
db.SaveChanges();
}
// 重新生成默认点位
int pointCount = CalculatePointCount(_homeWidth);
CreateDefaultPoints(pointCount);
UpdateStatus("已重置为默认点位置");
}
catch (Exception ex)
{
UpdateStatus($"重置失败: {ex.Message}");
}
}
/// <summary>
/// 更新状态栏文本
/// </summary>
private void UpdateStatus(string message)
{
StatusText.Text = $"{DateTime.Now:HH:mm:ss} - {message}";
}
#endregion
}
/// <summary>
/// 跳绳点位数据模型
/// </summary>
public class JumpRopePoint
{
/// <summary>
/// 点位编号 (1-7)
/// </summary>
public int Id { get; set; }
/// <summary>
/// 索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// X 坐标(固定不可移动)
/// </summary>
public double X { get; set; }
/// <summary>
/// Y 坐标(可拖动调整)
/// </summary>
public double Y { get; set; }
/// <summary>
/// 背景颜色(用于列表显示)
/// </summary>
public string BackgroundColor { get; set; }
}
}