Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ab3ccb6a6 |
9 changed files with 257 additions and 424 deletions
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<!-- Главная страница приложения -->
|
<!-- Главная страница приложения -->
|
||||||
<FlyoutItem Title="Главная" Icon="add.png">
|
<FlyoutItem Title="Главная" Icon="add.png">
|
||||||
<ShellContent ContentTemplate="{DataTemplate local:ResizableCanvasPage}" />
|
<ShellContent ContentTemplate="{DataTemplate local:GraphPage}" />
|
||||||
</FlyoutItem>
|
</FlyoutItem>
|
||||||
|
|
||||||
<!-- Выпадающий список категорий -->
|
<!-- Выпадающий список категорий -->
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ namespace NotesDataAnalyst
|
||||||
|
|
||||||
// Отрисовка текста
|
// Отрисовка текста
|
||||||
canvas.FontColor = Colors.Black;
|
canvas.FontColor = Colors.Black;
|
||||||
canvas.DrawString(node.Text, (float)node.Position.X + 50, (float)node.Position.Y + 10, HorizontalAlignment.Center);
|
canvas.DrawString(node.Text, (float)node.Position.X + 5, (float)node.Position.Y + 5, HorizontalAlignment.Left);
|
||||||
|
|
||||||
// Если нода выбрана, рисуем пунктирную обводку и "ручку" для изменения размера
|
// Если нода выбрана, рисуем пунктирную обводку и "ручку" для изменения размера
|
||||||
if (node.IsActive)
|
if (node.IsActive)
|
||||||
|
|
@ -70,8 +70,8 @@ namespace NotesDataAnalyst
|
||||||
// Ручка для изменения размера
|
// Ручка для изменения размера
|
||||||
canvas.FillColor = Colors.Gray;
|
canvas.FillColor = Colors.Gray;
|
||||||
canvas.FillRectangle(
|
canvas.FillRectangle(
|
||||||
(float)(node.Position.X + node.Size.Width - 0),
|
(float)(node.Position.X + node.Size.Width - 10),
|
||||||
(float)(node.Position.Y + node.Size.Height - 1),
|
(float)(node.Position.Y + node.Size.Height - 10),
|
||||||
10, 10);
|
10, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,26 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
|
||||||
x:Class="NotesDataAnalyst.GraphPage"
|
x:Class="NotesDataAnalyst.GraphPage"
|
||||||
Title="GraphPage">
|
Title="GraphPage">
|
||||||
|
|
||||||
|
<ContentPage.ToolbarItems>
|
||||||
|
<ToolbarItem Text="Add Node" Clicked="OnAddNodeClicked"/>
|
||||||
|
<ToolbarItem Text="Добавить дочернюю" Clicked="OnChildClicked"/>
|
||||||
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<Grid Padding="10" RowDefinitions="*,Auto">
|
|
||||||
<!-- Канва для рисования в отдельном ContentView -->
|
|
||||||
<ContentView Grid.Row="0">
|
|
||||||
<ContentView.GestureRecognizers>
|
|
||||||
<PanGestureRecognizer PanUpdated="OnPanUpdated"/>
|
|
||||||
<PinchGestureRecognizer PinchUpdated="OnPinchUpdated"/>
|
|
||||||
</ContentView.GestureRecognizers>
|
|
||||||
<AbsoluteLayout>
|
<AbsoluteLayout>
|
||||||
<GraphicsView x:Name="canvasView"
|
<GraphicsView x:Name="canvasView"
|
||||||
HeightRequest="8000"
|
HeightRequest="800"
|
||||||
WidthRequest="8000"/>
|
WidthRequest="800" />
|
||||||
</AbsoluteLayout>
|
|
||||||
</ContentView>
|
|
||||||
<ContentView Grid.Row="1">
|
|
||||||
<!-- Панель с кнопками внизу -->
|
|
||||||
<ScrollView Grid.Row="1" Orientation="Horizontal" HorizontalScrollBarVisibility="Always" VerticalScrollBarVisibility="Never">
|
|
||||||
<StackLayout Orientation="Horizontal" Spacing="1" Padding="20">
|
|
||||||
<Button Text="Добавить" Clicked="OnAddNodeClicked" WidthRequest="100"/>
|
|
||||||
<Button Text="Добавить дочернюю" Clicked="OnChildClicked" />
|
|
||||||
<Button Text="Изменить размер" Clicked="OnResizeNodeClicked" />
|
|
||||||
<Button Text="Редактировать текст" Clicked="OnEditTextClicked" />
|
|
||||||
</StackLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</ContentView>
|
|
||||||
<!-- Поле для редактирования текста -->
|
<!-- Поле для редактирования текста -->
|
||||||
<Entry x:Name="textEditor"
|
<Entry x:Name="textEditor"
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
BackgroundColor="White"
|
BackgroundColor="White"
|
||||||
TextColor="Black"
|
TextColor="Black"
|
||||||
FontSize="18"
|
FontSize="18" />
|
||||||
Grid.Row="1" />
|
</AbsoluteLayout>
|
||||||
</Grid>
|
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
@ -1,41 +1,34 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace NotesDataAnalyst
|
namespace NotesDataAnalyst
|
||||||
{
|
{
|
||||||
|
|
||||||
public partial class GraphPage : ContentPage
|
public partial class GraphPage : ContentPage
|
||||||
{
|
{
|
||||||
private Point _initialPanPosition;
|
|
||||||
private PointF _initialNodePosition;
|
private PointF _initialNodePosition; // Õðàíèò íà÷àëüíóþ ïîçèöèþ íîäû
|
||||||
private PointF _initialTouchPosition;
|
private PointF _initialTouchPosition; // Õðàíèò íà÷àëüíóþ ïîçèöèþ êàñàíèÿ
|
||||||
private SizeF _initialNodeSize;
|
private SizeF _initialNodeSize; // Õðàíèò íà÷àëüíûé ðàçìåð íîäû
|
||||||
|
private DateTime tapStartTime;
|
||||||
|
private const int longPressDuration = 3000; // 3 ñåêóíäû â ìèëëèñåêóíäàõ
|
||||||
|
|
||||||
private readonly List<GraphNode> _nodes = new();
|
private readonly List<GraphNode> _nodes = new();
|
||||||
private readonly List<GraphConnection> _connections = new();
|
private readonly List<GraphConnection> _connections = new();
|
||||||
private GraphNode _selectedNode;
|
private GraphNode _selectedNode;
|
||||||
private bool _isScrollingBlocked = false;
|
private bool _isResizing = false;
|
||||||
|
|
||||||
private double _scale = 1.0;
|
|
||||||
private double _initialScale = 1.0;
|
|
||||||
private bool _isNodeBeingMoved = false;
|
|
||||||
private const double SmoothFactor = 0.85; // Коэффициент сглаживания
|
|
||||||
private Point _lastTranslation;
|
|
||||||
// Переменная для хранения центральной точки жеста (между двумя пальцами при старте масштабирования)
|
|
||||||
private Point _gestureCenter;
|
|
||||||
|
|
||||||
private Point _canvasTranslation = new Point(0, 0);
|
|
||||||
|
|
||||||
double currentScale = 1;
|
|
||||||
double startScale = 1;
|
|
||||||
double xOffset = 0;
|
|
||||||
double yOffset = 0;
|
|
||||||
|
|
||||||
public GraphPage()
|
public GraphPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Íàñòðàèâàåì Drawable äëÿ îòðèñîâêè óçëîâ è ñâÿçåé
|
||||||
var graphDrawable = new GraphDrawable(_nodes, _connections);
|
var graphDrawable = new GraphDrawable(_nodes, _connections);
|
||||||
canvasView.Drawable = graphDrawable;
|
canvasView.Drawable = graphDrawable;
|
||||||
|
|
||||||
|
// Äîáàâëÿåì æåñòû
|
||||||
var panGesture = new PanGestureRecognizer();
|
var panGesture = new PanGestureRecognizer();
|
||||||
panGesture.PanUpdated += OnPanUpdated;
|
panGesture.PanUpdated += OnPanUpdated;
|
||||||
canvasView.GestureRecognizers.Add(panGesture);
|
canvasView.GestureRecognizers.Add(panGesture);
|
||||||
|
|
@ -44,232 +37,91 @@ namespace NotesDataAnalyst
|
||||||
tapGesture.Tapped += OnCanvasTapped;
|
tapGesture.Tapped += OnCanvasTapped;
|
||||||
canvasView.GestureRecognizers.Add(tapGesture);
|
canvasView.GestureRecognizers.Add(tapGesture);
|
||||||
|
|
||||||
// Добавляем Pinch только один раз
|
var tapGestureRecognizer = new TapGestureRecognizer();
|
||||||
if (!canvasView.GestureRecognizers.OfType<PinchGestureRecognizer>().Any())
|
tapGestureRecognizer.Tapped += OnTapped;
|
||||||
|
canvasView.GestureRecognizers.Add(tapGestureRecognizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void OnTapped(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var pinchGesture = new PinchGestureRecognizer();
|
tapStartTime = DateTime.Now;
|
||||||
pinchGesture.PinchUpdated += OnPinchUpdated;
|
|
||||||
canvasView.GestureRecognizers.Add(pinchGesture);
|
Device.StartTimer(TimeSpan.FromMilliseconds(longPressDuration), () =>
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - tapStartTime).TotalMilliseconds >= longPressDuration)
|
||||||
|
{
|
||||||
|
// Çäåñü âûïîëíÿåòñÿ äåéñòâèå ïðè äëèòåëüíîì íàæàòèè
|
||||||
|
OnNodeLongPressed();
|
||||||
|
}
|
||||||
|
return false; // Îñòàíàâëèâàåì òàéìåð
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnNodeLongPressed()
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Обработчик жеста перемещения (Pan) для перемещения канвы или ноды
|
|
||||||
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (_selectedNode != null)
|
if (_selectedNode != null)
|
||||||
{
|
{
|
||||||
// Начало перетаскивания
|
// Äîáàâëÿåì äî÷åðíþþ íîäó ðÿäîì ñ àêòèâíîé
|
||||||
if (e.StatusType == GestureStatus.Started)
|
var childNode = new GraphNode
|
||||||
{
|
{
|
||||||
// Запоминаем начальную позицию касания и ноды
|
Text = "Äî÷åðíÿÿ íîäà",
|
||||||
_initialTouchPosition = new Point(e.TotalX, e.TotalY);
|
Position = new Point(_selectedNode.Position.X + 50, _selectedNode.Position.Y + 50),
|
||||||
_initialNodePosition = _selectedNode.Position;
|
Size = new Size(150, 100)
|
||||||
|
};
|
||||||
|
|
||||||
|
_nodes.Add(childNode);
|
||||||
|
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = childNode });
|
||||||
|
|
||||||
|
canvasView.Invalidate(); // Ïåðåðèñîâûâàåì
|
||||||
}
|
}
|
||||||
// Перемещение
|
|
||||||
else if (e.StatusType == GestureStatus.Running)
|
// Çäåñü ðåàëèçóéòå äåéñòâèå ïðè äëèòåëüíîì íàæàòèè
|
||||||
|
// DisplayAlert("Äëèòåëüíîå íàæàòèå", "Âû íàæàëè è óäåðæèâàëè ýëåìåíò", "ÎÊ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeTapped(object sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
var deltaX = e.TotalX - _initialTouchPosition.X; // Разница с начальной точкой
|
var node = _selectedNode;
|
||||||
var deltaY = e.TotalY - _initialTouchPosition.Y;
|
|
||||||
|
|
||||||
// Перемещаем ноду относительно её начальной позиции
|
Debug.WriteLine("Íàæàòà íîäà - {node}");
|
||||||
_selectedNode.Position = new Point(
|
|
||||||
_initialNodePosition.X + deltaX,
|
|
||||||
_initialNodePosition.Y + deltaY
|
|
||||||
);
|
|
||||||
|
|
||||||
canvasView.Invalidate(); // Перерисовываем канву
|
|
||||||
}
|
}
|
||||||
// Завершение перетаскивания
|
private void OnChildClicked(object sender, EventArgs e)
|
||||||
else if (e.StatusType == GestureStatus.Completed)
|
|
||||||
{
|
|
||||||
// Можно добавить логику на завершение перетаскивания, например, включить прокрутку
|
|
||||||
//scrollView.IsEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else // Перемещение канвы при снятии выделения с ноды
|
|
||||||
{
|
|
||||||
if (e.StatusType == GestureStatus.Started)
|
|
||||||
{
|
|
||||||
_initialPanPosition = new Point(canvasView.TranslationX - e.TotalX, canvasView.TranslationY - e.TotalY);
|
|
||||||
}
|
|
||||||
else if (e.StatusType == GestureStatus.Running)
|
|
||||||
{
|
|
||||||
var deltaX = e.TotalX;
|
|
||||||
var deltaY = e.TotalY;
|
|
||||||
|
|
||||||
// Применяем сглаживание к движению
|
|
||||||
double smoothedX = _lastTranslation.X + SmoothFactor * (deltaX - _lastTranslation.X);
|
|
||||||
double smoothedY = _lastTranslation.Y + SmoothFactor * (deltaY - _lastTranslation.Y);
|
|
||||||
|
|
||||||
// Обновляем положение канвы
|
|
||||||
canvasView.TranslationX = _initialPanPosition.X + smoothedX;
|
|
||||||
canvasView.TranslationY = _initialPanPosition.Y + smoothedY;
|
|
||||||
|
|
||||||
// Сохраняем последнюю смягченную позицию для следующего цикла
|
|
||||||
_lastTranslation = new Point(smoothedX, smoothedY);
|
|
||||||
}
|
|
||||||
else if (e.StatusType == GestureStatus.Completed)
|
|
||||||
{
|
|
||||||
_initialPanPosition = new Point(canvasView.TranslationX, canvasView.TranslationY);
|
|
||||||
canvasView.Invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Status == GestureStatus.Started)
|
|
||||||
{
|
|
||||||
// Store the current scale factor applied to the wrapped user interface element,
|
|
||||||
// and zero the components for the center point of the translate transform.
|
|
||||||
startScale = Content.Scale;
|
|
||||||
Content.AnchorX = 0;
|
|
||||||
Content.AnchorY = 0;
|
|
||||||
}
|
|
||||||
if (e.Status == GestureStatus.Running)
|
|
||||||
{
|
|
||||||
// Calculate the scale factor to be applied.
|
|
||||||
currentScale += (e.Scale - 1) * startScale;
|
|
||||||
currentScale = Math.Max(1, currentScale);
|
|
||||||
|
|
||||||
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
|
|
||||||
// so get the X pixel coordinate.
|
|
||||||
double renderedX = Content.X + xOffset;
|
|
||||||
double deltaX = renderedX / Width;
|
|
||||||
double deltaWidth = Width / (Content.Width * startScale);
|
|
||||||
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
|
|
||||||
|
|
||||||
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
|
|
||||||
// so get the Y pixel coordinate.
|
|
||||||
double renderedY = Content.Y + yOffset;
|
|
||||||
double deltaY = renderedY / Height;
|
|
||||||
double deltaHeight = Height / (Content.Height * startScale);
|
|
||||||
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
|
|
||||||
|
|
||||||
// Calculate the transformed element pixel coordinates.
|
|
||||||
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
|
|
||||||
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
|
|
||||||
|
|
||||||
// Apply translation based on the change in origin.
|
|
||||||
Content.TranslationX = Math.Clamp(targetX, -Content.Width * (currentScale - 1), 0);
|
|
||||||
Content.TranslationY = Math.Clamp(targetY, -Content.Height * (currentScale - 1), 0);
|
|
||||||
|
|
||||||
// Apply scale factor
|
|
||||||
Content.Scale = currentScale;
|
|
||||||
}
|
|
||||||
if (e.Status == GestureStatus.Completed)
|
|
||||||
{
|
|
||||||
// Store the translation delta's of the wrapped user interface element.
|
|
||||||
xOffset = Content.TranslationX;
|
|
||||||
yOffset = Content.TranslationY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async void OnEditTextClicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Проверяем, выбрана ли нода
|
|
||||||
if (_selectedNode == null)
|
|
||||||
{
|
|
||||||
await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Показываем поле для редактирования текста и заполняем его текущим текстом ноды
|
|
||||||
textEditor.IsVisible = true;
|
|
||||||
textEditor.Text = _selectedNode.Text;
|
|
||||||
|
|
||||||
// Ставим фокус на текстовое поле для ввода
|
|
||||||
textEditor.Focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик завершения редактирования текста
|
|
||||||
private void OnTextEditorCompleted(object sender, EventArgs e)
|
|
||||||
{
|
{
|
||||||
if (_selectedNode != null)
|
if (_selectedNode != null)
|
||||||
{
|
{
|
||||||
// Обновляем текст ноды с введенным значением
|
var child = new GraphNode
|
||||||
_selectedNode.Text = textEditor.Text;
|
{
|
||||||
|
Text = "Child Node",
|
||||||
// Скрываем поле для редактирования текста
|
Position = new Point(_selectedNode.Position.X + 5, _selectedNode.Position.Y + 5),
|
||||||
textEditor.IsVisible = false;
|
Size = _selectedNode.Size,
|
||||||
|
BackgroundColor = Colors.Gray,
|
||||||
// Перерисовываем канву для обновления отображения текста
|
};
|
||||||
|
_nodes.Add(child);
|
||||||
|
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = child });
|
||||||
canvasView.Invalidate();
|
canvasView.Invalidate();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnResizeNodeClicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Проверяем, выбрана ли нода
|
|
||||||
if (_selectedNode == null)
|
|
||||||
{
|
|
||||||
await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Открываем диалог для ввода новых размеров
|
|
||||||
var result = await DisplayPromptAsync("Изменить размер", "Введите ширину и высоту",
|
|
||||||
initialValue: $"{_selectedNode.Size.Width} x {_selectedNode.Size.Height}",
|
|
||||||
keyboard: Keyboard.Numeric);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result)) return;
|
|
||||||
|
|
||||||
// Разбиваем введенное значение на ширину и высоту
|
|
||||||
var dimensions = result.Split('x');
|
|
||||||
if (dimensions.Length == 2 &&
|
|
||||||
double.TryParse(dimensions[0].Trim(), out double width) &&
|
|
||||||
double.TryParse(dimensions[1].Trim(), out double height))
|
|
||||||
{
|
|
||||||
// Обновляем размеры ноды
|
|
||||||
_selectedNode.Size = new Size(width, height);
|
|
||||||
canvasView.Invalidate(); // Перерисовываем канву
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await DisplayAlert("Ошибка", "Неверный формат. Введите значения в формате ширина x высота.", "OK");
|
|
||||||
}
|
}
|
||||||
|
else { Debug.WriteLine("Ñíà÷àëà âûáèðèòå íîäó"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnAddNodeClicked(object sender, EventArgs e)
|
private async void OnAddNodeClicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
var newNode = new GraphNode
|
var newNode = new GraphNode
|
||||||
{
|
{
|
||||||
Text = "Новая нода",
|
Text = "Íîâàÿ íîäà",
|
||||||
Position = new Point(100, 200),
|
Position = new Point(300, 200),
|
||||||
Size = new Size(100, 50),
|
Size = new Size(200, 200),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_nodes.Add(newNode);
|
_nodes.Add(newNode);
|
||||||
canvasView.Invalidate();
|
canvasView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChildClicked(object sender, EventArgs e)
|
// Îáðàáîò÷èê äëÿ âûáîðà óçëà
|
||||||
{
|
// Îáðàáîò÷èê äëÿ âûáîðà óçëà
|
||||||
if (_selectedNode != null)
|
|
||||||
{
|
|
||||||
var childNode = new GraphNode
|
|
||||||
{
|
|
||||||
Text = "Child Node",
|
|
||||||
Position = new Point(_selectedNode.Position.X + 100, _selectedNode.Position.Y + 70),
|
|
||||||
Size = _selectedNode.Size,
|
|
||||||
BackgroundColor = Colors.Gray,
|
|
||||||
};
|
|
||||||
_nodes.Add(childNode);
|
|
||||||
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = childNode });
|
|
||||||
canvasView.Invalidate();
|
|
||||||
_selectedNode = childNode;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.WriteLine("Сначала выберите ноду");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCanvasTapped(object sender, TappedEventArgs e)
|
private void OnCanvasTapped(object sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
var tapPosition = e.GetPosition((View)sender);
|
var tapPosition = e.GetPosition((View)sender);
|
||||||
|
|
@ -280,16 +132,141 @@ namespace NotesDataAnalyst
|
||||||
var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y);
|
var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y);
|
||||||
_selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node));
|
_selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node));
|
||||||
|
|
||||||
|
// Ñáðîñ àêòèâíîñòè äëÿ âñåõ óçëîâ, êðîìå âûáðàííîãî
|
||||||
foreach (var node in _nodes)
|
foreach (var node in _nodes)
|
||||||
node.IsActive = node == _selectedNode;
|
node.IsActive = node == _selectedNode;
|
||||||
|
|
||||||
|
// Îáíîâëÿåì ýêðàí ïîñëå èçìåíåíèÿ ñîñòîÿíèÿ óçëîâ
|
||||||
canvasView.Invalidate();
|
canvasView.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selectedNode == null) return;
|
||||||
|
|
||||||
|
if (e.StatusType == GestureStatus.Started)
|
||||||
|
{
|
||||||
|
// Ñîõðàíÿåì íà÷àëüíóþ ïîçèöèþ íîäû è ðàçìåð
|
||||||
|
_initialNodePosition = _selectedNode.Position;
|
||||||
|
_initialTouchPosition = new PointF((float)e.TotalX, (float)e.TotalY);
|
||||||
|
_initialNodeSize = _selectedNode.Size; // Ñîõðàíÿåì íà÷àëüíûé ðàçìåð
|
||||||
|
|
||||||
|
// Ïðîâåðÿåì, íàõîäèìñÿ ëè ìû íà ðó÷êå èçìåíåíèÿ ðàçìåðà
|
||||||
|
_isResizing = IsOnRightResizeHandle(_selectedNode, _initialTouchPosition) ||
|
||||||
|
IsOnLeftResizeHandle(_selectedNode, _initialTouchPosition) ||
|
||||||
|
IsOnTopResizeHandle(_selectedNode, _initialTouchPosition) ||
|
||||||
|
IsOnBottomResizeHandle(_selectedNode, _initialTouchPosition);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (e.StatusType == GestureStatus.Running)
|
||||||
|
{
|
||||||
|
// Âû÷èñëÿåì ñìåùåíèå îò íà÷àëüíîé ïîçèöèè êàñàíèÿ
|
||||||
|
var deltaX = (float)e.TotalX - _initialTouchPosition.X;
|
||||||
|
var deltaY = (float)e.TotalY - _initialTouchPosition.Y;
|
||||||
|
|
||||||
|
if (_isResizing)
|
||||||
|
{
|
||||||
|
ResizeNode(_selectedNode, deltaX, deltaY);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Ïåðåìåùåíèå
|
||||||
|
_selectedNode.Position = new PointF(
|
||||||
|
_initialNodePosition.X + deltaX,
|
||||||
|
_initialNodePosition.Y + deltaY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasView.Invalidate();
|
||||||
|
}
|
||||||
|
else if (e.StatusType == GestureStatus.Completed)
|
||||||
|
{
|
||||||
|
_isResizing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Èçìåíåíèå ðàçìåðà óçëà
|
||||||
|
private void ResizeNode(GraphNode node, double deltaX, double deltaY)
|
||||||
|
{
|
||||||
|
const float minSize = 50f; // Óáèðàåì "f" äëÿ double
|
||||||
|
|
||||||
|
// Óâåëè÷èâàåì èëè óìåíüøàåì ðàçìåð â çàâèñèìîñòè îò íàïðàâëåíèÿ
|
||||||
|
if (_isResizing)
|
||||||
|
{
|
||||||
|
// Åñëè ìû òÿíåì ïðàâóþ "ðó÷êó", óâåëè÷èâàåì øèðèíó
|
||||||
|
if (IsOnRightResizeHandle(node, _initialTouchPosition))
|
||||||
|
{
|
||||||
|
_selectedNode.Size = new Size(
|
||||||
|
Math.Max(minSize, node.Size.Width + deltaX), // double
|
||||||
|
_selectedNode.Size.Height // double
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Åñëè ìû òÿíåì ëåâóþ "ðó÷êó", óìåíüøàåì øèðèíó
|
||||||
|
else if (IsOnLeftResizeHandle(node, _initialTouchPosition))
|
||||||
|
{
|
||||||
|
_selectedNode.Size = new Size(
|
||||||
|
Math.Max(minSize, _selectedNode.Size.Width - deltaX), // double
|
||||||
|
_selectedNode.Size.Height // double
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ïåðåìåùàåì íîäó âëåâî, ÷òîáû ñîõðàíèòü åå ïîçèöèþ
|
||||||
|
_selectedNode.Position = new Point(_selectedNode.Position.X + deltaX, _selectedNode.Position.Y); // double
|
||||||
|
}
|
||||||
|
// Åñëè ìû òÿíåì âåðõíþþ "ðó÷êó", óìåíüøàåì âûñîòó
|
||||||
|
else if (IsOnTopResizeHandle(node, _initialTouchPosition))
|
||||||
|
{
|
||||||
|
_selectedNode.Size = new Size(
|
||||||
|
_selectedNode.Size.Width, // double
|
||||||
|
Math.Max(minSize, _selectedNode.Size.Height - deltaY) // double
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ïåðåìåùàåì íîäó ââåðõ, ÷òîáû ñîõðàíèòü åå ïîçèöèþ
|
||||||
|
_selectedNode.Position = new Point(_selectedNode.Position.X, _selectedNode.Position.Y + deltaY); // double
|
||||||
|
}
|
||||||
|
// Åñëè ìû òÿíåì íèæíþþ "ðó÷êó", óâåëè÷èâàåì âûñîòó
|
||||||
|
else if (IsOnBottomResizeHandle(node, _initialTouchPosition))
|
||||||
|
{
|
||||||
|
_selectedNode.Size = new Size(
|
||||||
|
_selectedNode.Size.Width, // double
|
||||||
|
Math.Max(minSize, _selectedNode.Size.Height + deltaY) // double
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool IsOnRightResizeHandle(GraphNode node, PointF point)
|
||||||
|
{
|
||||||
|
const float handleSize = 400;
|
||||||
|
return Math.Abs(point.X - (node.Position.X + node.Size.Width)) <= handleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOnLeftResizeHandle(GraphNode node, PointF point)
|
||||||
|
{
|
||||||
|
const float handleSize = 400;
|
||||||
|
return Math.Abs(point.X - node.Position.X) <= handleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOnTopResizeHandle(GraphNode node, PointF point)
|
||||||
|
{
|
||||||
|
const float handleSize = 400;
|
||||||
|
return Math.Abs(point.Y - node.Position.Y) <= handleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOnBottomResizeHandle(GraphNode node, PointF point)
|
||||||
|
{
|
||||||
|
const float handleSize = 400;
|
||||||
|
return Math.Abs(point.Y - (node.Position.Y + node.Size.Height)) <= handleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Ìåòîä äëÿ ïðîâåðêè, íàõîäèòñÿ ëè òî÷êà âíóòðè óçëà
|
||||||
private bool IsPointInsideNode(PointF point, GraphNode node)
|
private bool IsPointInsideNode(PointF point, GraphNode node)
|
||||||
{
|
{
|
||||||
return point.X >= node.Position.X &&
|
return point.X >= node.Position.X &&
|
||||||
|
|
@ -297,5 +274,42 @@ namespace NotesDataAnalyst
|
||||||
point.Y >= node.Position.Y &&
|
point.Y >= node.Position.Y &&
|
||||||
point.Y <= node.Position.Y + node.Size.Height;
|
point.Y <= node.Position.Y + node.Size.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Ïðîâåðêà, íàõîäèòñÿ ëè òî÷êà íà "ðó÷êå" èçìåíåíèÿ ðàçìåðà
|
||||||
|
private bool IsOnResizeHandle(GraphNode node, PointF point)
|
||||||
|
{
|
||||||
|
const float handleSize = 200f; // Ðàçìåð ðó÷êè èçìåíåíèÿ ðàçìåðà
|
||||||
|
|
||||||
|
// Îïðåäåëÿåì ïðÿìîóãîëüíèêè äëÿ êàæäîé ðó÷êè
|
||||||
|
var rightHandleRect = new RectF(
|
||||||
|
(float)(node.Position.X + node.Size.Width - handleSize),
|
||||||
|
(float)(node.Position.Y + node.Size.Height - handleSize),
|
||||||
|
handleSize, handleSize);
|
||||||
|
|
||||||
|
var leftHandleRect = new RectF(
|
||||||
|
(float)(node.Position.X),
|
||||||
|
(float)(node.Position.Y + node.Size.Height - handleSize),
|
||||||
|
handleSize, handleSize);
|
||||||
|
|
||||||
|
var topHandleRect = new RectF(
|
||||||
|
(float)(node.Position.X + (node.Size.Width / 2) - (handleSize / 2)),
|
||||||
|
(float)(node.Position.Y),
|
||||||
|
handleSize, handleSize);
|
||||||
|
|
||||||
|
var bottomHandleRect = new RectF(
|
||||||
|
(float)(node.Position.X + (node.Size.Width / 2) - (handleSize / 2)),
|
||||||
|
(float)(node.Position.Y + node.Size.Height - handleSize),
|
||||||
|
handleSize, handleSize);
|
||||||
|
|
||||||
|
// Ïðîâåðÿåì, íàõîäèòñÿ ëè êóðñîð íà îäíîé èç ðó÷åê
|
||||||
|
return rightHandleRect.Contains(point) ||
|
||||||
|
leftHandleRect.Contains(point) ||
|
||||||
|
topHandleRect.Contains(point) ||
|
||||||
|
bottomHandleRect.Contains(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using CommunityToolkit.Maui;
|
||||||
|
|
||||||
namespace NotesDataAnalyst
|
namespace NotesDataAnalyst
|
||||||
{
|
{
|
||||||
|
//Пиздец!
|
||||||
public static class MauiProgram
|
public static class MauiProgram
|
||||||
{
|
{
|
||||||
public static MauiApp CreateMauiApp()
|
public static MauiApp CreateMauiApp()
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,6 @@
|
||||||
<MauiXaml Update="NoteContent\NoteSelectionPopup.xaml">
|
<MauiXaml Update="NoteContent\NoteSelectionPopup.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</MauiXaml>
|
</MauiXaml>
|
||||||
<MauiXaml Update="ResizableCanvasPage.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</MauiXaml>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace NotesDataAnalyst
|
|
||||||
{
|
|
||||||
internal class PinchToZoomContainer
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="NotesDataAnalyst.ResizableCanvasPage"
|
|
||||||
Title="ResizableCanvasPage">
|
|
||||||
<ScrollView>
|
|
||||||
<AbsoluteLayout x:Name="CanvasLayout"
|
|
||||||
WidthRequest="2000" HeightRequest="2000">
|
|
||||||
<!-- Элементы будут добавляться программно -->
|
|
||||||
</AbsoluteLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
using Microsoft.Maui.Controls;
|
|
||||||
using Microsoft.Maui.Graphics;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace NotesDataAnalyst
|
|
||||||
{
|
|
||||||
public partial class ResizableCanvasPage : ContentPage
|
|
||||||
{
|
|
||||||
private const double InitialWidth = 150;
|
|
||||||
private const double InitialHeight = 100;
|
|
||||||
private double startX, startY;
|
|
||||||
|
|
||||||
public ResizableCanvasPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddResizableObject(double x, double y)
|
|
||||||
{
|
|
||||||
// Ñîçäàåì îñíîâíîé êîíòåéíåð (Frame) äëÿ îáúåêòà è ðó÷êè
|
|
||||||
var resizableFrame = new Frame
|
|
||||||
{
|
|
||||||
BackgroundColor = Colors.LightBlue,
|
|
||||||
BorderColor = Colors.DarkBlue,
|
|
||||||
CornerRadius = 8,
|
|
||||||
WidthRequest = InitialWidth,
|
|
||||||
HeightRequest = InitialHeight,
|
|
||||||
Padding = 0, // Óáèðàåì âíóòðåííèå îòñòóïû
|
|
||||||
Content = new Grid() // Èñïîëüçóåì Grid äëÿ äîáàâëåíèÿ ñîäåðæèìîãî è ðó÷êè
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ñîçäàåì ñîäåðæèìîå âíóòðè ôðåéìà
|
|
||||||
var content = new StackLayout
|
|
||||||
{
|
|
||||||
BackgroundColor = Colors.LightBlue,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label { Text = "Resizable Object", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var resizeHandle = new BoxView
|
|
||||||
{
|
|
||||||
BackgroundColor = Colors.Gray,
|
|
||||||
WidthRequest = 20,
|
|
||||||
HeightRequest = 20
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ñîçäàåì Grid âíóòðè Frame
|
|
||||||
var grid = (Grid)resizableFrame.Content;
|
|
||||||
|
|
||||||
// Íàñòðîèì ñåòêó: äîáàâèì äâà ñòîëáöà è äâå ñòðîêè
|
|
||||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
|
|
||||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // äëÿ ðó÷êè
|
|
||||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
|
|
||||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // äëÿ ðó÷êè
|
|
||||||
|
|
||||||
// Äîáàâëÿåì ñîäåðæèìîå â ïåðâóþ ÿ÷åéêó (ñòîëáåö 0, ñòðîêà 0)
|
|
||||||
grid.Children.Add(content); // Áåç óêàçàíèÿ êîëîíîê/ñòðîê
|
|
||||||
|
|
||||||
// Äîáàâëÿåì ðó÷êó â ïðàâûé íèæíèé óãîë (ñòîëáåö 1, ñòðîêà 1)
|
|
||||||
grid.Children.Add(resizeHandle); // Áåç óêàçàíèÿ êîëîíîê/ñòðîê
|
|
||||||
|
|
||||||
// Óñòàíàâëèâàåì ñòðîêó è êîëîíêó äëÿ ðó÷êè
|
|
||||||
Grid.SetRow(resizeHandle, 1);
|
|
||||||
Grid.SetColumn(resizeHandle, 1);
|
|
||||||
|
|
||||||
// Äîáàâëÿåì ïàíîðàìó äëÿ ïåðåìåùåíèÿ îáúåêòà
|
|
||||||
var panGesture = new PanGestureRecognizer();
|
|
||||||
panGesture.PanUpdated += (s, e) => OnDragPanUpdated(s, e, resizableFrame);
|
|
||||||
resizableFrame.GestureRecognizers.Add(panGesture);
|
|
||||||
|
|
||||||
// Äîáàâëÿåì ïàíîðàìó äëÿ èçìåíåíèÿ ðàçìåðà
|
|
||||||
var resizePanGesture = new PanGestureRecognizer();
|
|
||||||
resizePanGesture.PanUpdated += (s, e) => OnResizePanUpdated(s, e, resizableFrame, resizeHandle);
|
|
||||||
resizeHandle.GestureRecognizers.Add(resizePanGesture);
|
|
||||||
|
|
||||||
// Óñòàíàâëèâàåì ïîçèöèþ ôðåéìà
|
|
||||||
AbsoluteLayout.SetLayoutBounds(resizableFrame, new Rect(x, y, InitialWidth, InitialHeight));
|
|
||||||
CanvasLayout.Children.Add(resizableFrame);
|
|
||||||
|
|
||||||
// Îáíîâëÿåì ïîçèöèþ ðó÷êè
|
|
||||||
UpdateResizeHandlePosition(resizableFrame, resizeHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragPanUpdated(object sender, PanUpdatedEventArgs e, Frame frame)
|
|
||||||
{
|
|
||||||
if (e.StatusType == GestureStatus.Started)
|
|
||||||
{
|
|
||||||
startX = AbsoluteLayout.GetLayoutBounds(frame).X;
|
|
||||||
startY = AbsoluteLayout.GetLayoutBounds(frame).Y;
|
|
||||||
}
|
|
||||||
else if (e.StatusType == GestureStatus.Running)
|
|
||||||
{
|
|
||||||
double x = startX + e.TotalX;
|
|
||||||
double y = startY + e.TotalY;
|
|
||||||
AbsoluteLayout.SetLayoutBounds(frame, new Rect(x, y, frame.WidthRequest, frame.HeightRequest));
|
|
||||||
|
|
||||||
// Îáíîâëÿåì ïîçèöèþ ðó÷êè
|
|
||||||
var resizeHandle = (BoxView)((Grid)frame.Content).Children[1];
|
|
||||||
UpdateResizeHandlePosition(frame, resizeHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnResizePanUpdated(object sender, PanUpdatedEventArgs e, Frame frame, BoxView resizeHandle)
|
|
||||||
{
|
|
||||||
if (e.StatusType == GestureStatus.Started)
|
|
||||||
{
|
|
||||||
startX = frame.WidthRequest;
|
|
||||||
startY = frame.HeightRequest;
|
|
||||||
}
|
|
||||||
else if (e.StatusType == GestureStatus.Running)
|
|
||||||
{
|
|
||||||
double newWidth = Math.Max(50, startX + e.TotalX);
|
|
||||||
double newHeight = Math.Max(50, startY + e.TotalY);
|
|
||||||
frame.WidthRequest = newWidth;
|
|
||||||
frame.HeightRequest = newHeight;
|
|
||||||
|
|
||||||
// Îáíîâëÿåì ïîçèöèþ ðó÷êè ïîñëå èçìåíåíèÿ ðàçìåðîâ
|
|
||||||
UpdateResizeHandlePosition(frame, resizeHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateResizeHandlePosition(Frame frame, BoxView resizeHandle)
|
|
||||||
{
|
|
||||||
var frameBounds = AbsoluteLayout.GetLayoutBounds(frame);
|
|
||||||
double x = frameBounds.X + frame.WidthRequest - resizeHandle.WidthRequest;
|
|
||||||
double y = frameBounds.Y + frame.HeightRequest - resizeHandle.HeightRequest;
|
|
||||||
|
|
||||||
AbsoluteLayout.SetLayoutBounds(resizeHandle, new Rect(x, y, resizeHandle.WidthRequest, resizeHandle.HeightRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnAppearing()
|
|
||||||
{
|
|
||||||
base.OnAppearing();
|
|
||||||
AddResizableObject(100, 100);
|
|
||||||
AddResizableObject(400, 200);
|
|
||||||
AddResizableObject(700, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue