Compare commits

...

10 commits

Author SHA1 Message Date
ARCHI86
c3fc868156 т 2024-11-11 18:07:11 +03:00
ARCHI86
5dbd13040f х 2024-11-11 17:59:20 +03:00
ARCHI86
70ed01f058 Да 2024-11-11 15:41:20 +03:00
ARCHI86
363a6f5ecb Норм 2024-11-11 15:24:11 +03:00
ARCHI86
4af0560f96 н 2024-11-11 12:44:56 +03:00
ARCHI86
5d23e7f97d хз 2024-11-11 01:30:39 +03:00
ARCHI86
ad2025c52b норм 2024-11-10 23:19:41 +03:00
ARCHI86
af9466c0a6 Хрен знает 2024-11-10 23:04:15 +03:00
ARCHI86
1303209095 Уже лучше 2024-11-10 16:44:47 +03:00
ARCHI86
db62966c32 Новая страница с нодами 2024-11-10 16:10:13 +03:00
8 changed files with 433 additions and 265 deletions

View file

@ -10,7 +10,7 @@
<!-- Главная страница приложения --> <!-- Главная страница приложения -->
<FlyoutItem Title="Главная" Icon="add.png"> <FlyoutItem Title="Главная" Icon="add.png">
<ShellContent ContentTemplate="{DataTemplate local:GraphPage}" /> <ShellContent ContentTemplate="{DataTemplate local:ResizableCanvasPage}" />
</FlyoutItem> </FlyoutItem>
<!-- Выпадающий список категорий --> <!-- Выпадающий список категорий -->

View file

@ -58,7 +58,7 @@ namespace NotesDataAnalyst
// Отрисовка текста // Отрисовка текста
canvas.FontColor = Colors.Black; canvas.FontColor = Colors.Black;
canvas.DrawString(node.Text, (float)node.Position.X + 5, (float)node.Position.Y + 5, HorizontalAlignment.Left); canvas.DrawString(node.Text, (float)node.Position.X + 50, (float)node.Position.Y + 10, HorizontalAlignment.Center);
// Если нода выбрана, рисуем пунктирную обводку и "ручку" для изменения размера // Если нода выбрана, рисуем пунктирную обводку и "ручку" для изменения размера
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 - 10), (float)(node.Position.X + node.Size.Width - 0),
(float)(node.Position.Y + node.Size.Height - 10), (float)(node.Position.Y + node.Size.Height - 1),
10, 10); 10, 10);
} }
} }

View file

@ -1,26 +1,40 @@
<?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>
<AbsoluteLayout> <Grid Padding="10" RowDefinitions="*,Auto">
<GraphicsView x:Name="canvasView" <!-- Канва для рисования в отдельном ContentView -->
HeightRequest="800" <ContentView Grid.Row="0">
WidthRequest="800" /> <ContentView.GestureRecognizers>
<!-- Поле для редактирования текста --> <PanGestureRecognizer PanUpdated="OnPanUpdated"/>
<Entry x:Name="textEditor" <PinchGestureRecognizer PinchUpdated="OnPinchUpdated"/>
IsVisible="False" </ContentView.GestureRecognizers>
BackgroundColor="White" <AbsoluteLayout>
TextColor="Black" <GraphicsView x:Name="canvasView"
FontSize="18" /> HeightRequest="8000"
</AbsoluteLayout> WidthRequest="8000"/>
</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"
IsVisible="False"
BackgroundColor="White"
TextColor="Black"
FontSize="18"
Grid.Row="1" />
</Grid>
</ContentPage.Content> </ContentPage.Content>
</ContentPage> </ContentPage>

View file

@ -1,34 +1,41 @@
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 _isResizing = false; private bool _isScrollingBlocked = 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);
@ -37,91 +44,232 @@ namespace NotesDataAnalyst
tapGesture.Tapped += OnCanvasTapped; tapGesture.Tapped += OnCanvasTapped;
canvasView.GestureRecognizers.Add(tapGesture); canvasView.GestureRecognizers.Add(tapGesture);
var tapGestureRecognizer = new TapGestureRecognizer(); // Добавляем Pinch только один раз
tapGestureRecognizer.Tapped += OnTapped; if (!canvasView.GestureRecognizers.OfType<PinchGestureRecognizer>().Any())
canvasView.GestureRecognizers.Add(tapGestureRecognizer);
}
private void OnTapped(object sender, EventArgs e)
{
tapStartTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromMilliseconds(longPressDuration), () =>
{ {
if ((DateTime.Now - tapStartTime).TotalMilliseconds >= longPressDuration) var pinchGesture = new PinchGestureRecognizer();
{ pinchGesture.PinchUpdated += OnPinchUpdated;
// Çäåñü âûïîëíÿåòñÿ äåéñòâèå ïðè äëèòåëüíîì íàæàòèè canvasView.GestureRecognizers.Add(pinchGesture);
OnNodeLongPressed();
}
return false; // Îñòàíàâëèâàåì òàéìåð
});
}
private void OnNodeLongPressed()
{
if (_selectedNode != null)
{
// Äîáàâëÿåì äî÷åðíþþ íîäó ðÿäîì ñ àêòèâíîé
var childNode = new GraphNode
{
Text = "Äî÷åðíÿÿ íîäà",
Position = new Point(_selectedNode.Position.X + 50, _selectedNode.Position.Y + 50),
Size = new Size(150, 100)
};
_nodes.Add(childNode);
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = childNode });
canvasView.Invalidate(); // Ïåðåðèñîâûâàåì
} }
// Çäåñü ðåàëèçóéòå äåéñòâèå ïðè äëèòåëüíîì íàæàòèè
// DisplayAlert("Äëèòåëüíîå íàæàòèå", "Âû íàæàëè è óäåðæèâàëè ýëåìåíò", "ÎÊ");
} }
private void OnNodeTapped(object sender, TappedEventArgs e)
{
var node = _selectedNode;
Debug.WriteLine("Íàæàòà íîäà - {node}"); // Обработчик жеста перемещения (Pan) для перемещения канвы или ноды
} private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
private void OnChildClicked(object sender, EventArgs e)
{ {
if (_selectedNode != null) if (_selectedNode != null)
{ {
var child = new GraphNode // Начало перетаскивания
if (e.StatusType == GestureStatus.Started)
{ {
Text = "Child Node", // Запоминаем начальную позицию касания и ноды
Position = new Point(_selectedNode.Position.X + 5, _selectedNode.Position.Y + 5), _initialTouchPosition = new Point(e.TotalX, e.TotalY);
Size = _selectedNode.Size, _initialNodePosition = _selectedNode.Position;
BackgroundColor = Colors.Gray, }
}; // Перемещение
_nodes.Add(child); else if (e.StatusType == GestureStatus.Running)
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = child }); {
canvasView.Invalidate(); var deltaX = e.TotalX - _initialTouchPosition.X; // Разница с начальной точкой
var deltaY = e.TotalY - _initialTouchPosition.Y;
} // Перемещаем ноду относительно её начальной позиции
else { Debug.WriteLine("Ñíà÷àëà âûáèðèòå íîäó"); } _selectedNode.Position = new Point(
_initialNodePosition.X + deltaX,
_initialNodePosition.Y + deltaY
);
canvasView.Invalidate(); // Перерисовываем канву
}
// Завершение перетаскивания
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)
{
// Обновляем текст ноды с введенным значением
_selectedNode.Text = textEditor.Text;
// Скрываем поле для редактирования текста
textEditor.IsVisible = false;
// Перерисовываем канву для обновления отображения текста
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");
}
} }
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(300, 200), Position = new Point(100, 200),
Size = new Size(200, 200), Size = new Size(100, 50),
}; };
_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);
@ -132,141 +280,16 @@ 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 &&
@ -274,42 +297,5 @@ 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);
}
} }
} }

View file

@ -84,6 +84,9 @@
<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>

12
PinchToZoomContainer.cs Normal file
View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NotesDataAnalyst
{
internal class PinchToZoomContainer
{
}
}

12
ResizableCanvasPage.xaml Normal file
View file

@ -0,0 +1,12 @@
<?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>

141
ResizableCanvasPage.xaml.cs Normal file
View file

@ -0,0 +1,141 @@
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);
}
}
}