Merge pull request #78 from david-c14/main

Add RectPainter sample
This commit is contained in:
Max Katz 2024-05-12 23:36:08 -07:00 коммит произвёл GitHub
Родитель cd8976eb6c d20bf64e3e
Коммит d56466f9dd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 624 добавлений и 8 удалений

Просмотреть файл

@ -101,6 +101,10 @@ Each sample is tagged with it's difficulty. The degree of difficulty describes h
| 🐔 Normal
| Game, Canvas, Game Loop, MVVM
| link:src/Avalonia.Samples/Drawing/RectPainter[Rect Painter Sample]
| 🐔 Normal
| Graphics, MVVM
|===
=== 🎞️ DataTemplate-Samples

Просмотреть файл

@ -1,4 +1,3 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
@ -40,20 +39,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Folder", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmDialogSample", "ViewInteraction\MvvmDialogSample\MvvmDialogSample.csproj", "{48432457-6A55-4D03-9D40-260CE8E06440}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DialogManagerSample", "ViewInteraction\DialogManagerSample\DialogManagerSample.csproj", "{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DialogManagerSample", "ViewInteraction\DialogManagerSample\DialogManagerSample.csproj", "{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{85B157B3-F701-4F75-B4F1-EC2287729480}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestableApp", "Testing\TestableApp\TestableApp.csproj", "{326EF526-6200-4570-90DE-5E6D48B63EAC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableApp", "Testing\TestableApp\TestableApp.csproj", "{326EF526-6200-4570-90DE-5E6D48B63EAC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestableApp.Headless.NUnit", "Testing\TestableApp.Headless.NUnit\TestableApp.Headless.NUnit.csproj", "{B8CB5C57-07ED-4BC6-ACE8-F05E428E3EB5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableApp.Headless.NUnit", "Testing\TestableApp.Headless.NUnit\TestableApp.Headless.NUnit.csproj", "{B8CB5C57-07ED-4BC6-ACE8-F05E428E3EB5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestableApp.Headless.XUnit", "Testing\TestableApp.Headless.XUnit\TestableApp.Headless.XUnit.csproj", "{BDA7536E-26FD-436F-AAC8-F8A2B500548E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableApp.Headless.XUnit", "Testing\TestableApp.Headless.XUnit\TestableApp.Headless.XUnit.csproj", "{BDA7536E-26FD-436F-AAC8-F8A2B500548E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestableApp.Appium", "Testing\TestableApp.Appium\TestableApp.Appium.csproj", "{F5CB3DA2-EB59-4792-A1B3-49F600F7C130}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableApp.Appium", "Testing\TestableApp.Appium\TestableApp.Appium.csproj", "{F5CB3DA2-EB59-4792-A1B3-49F600F7C130}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ViewInteraction", "ViewInteraction", "{2E99F15F-A82A-4734-A837-C0F768702600}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RectPainter", "Drawing\RectPainter\RectPainter.csproj", "{2B746401-384F-484A-810E-7A65288165E0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -124,6 +125,10 @@ Global
{F5CB3DA2-EB59-4792-A1B3-49F600F7C130}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5CB3DA2-EB59-4792-A1B3-49F600F7C130}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5CB3DA2-EB59-4792-A1B3-49F600F7C130}.Release|Any CPU.Build.0 = Release|Any CPU
{2B746401-384F-484A-810E-7A65288165E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B746401-384F-484A-810E-7A65288165E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B746401-384F-484A-810E-7A65288165E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B746401-384F-484A-810E-7A65288165E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -139,12 +144,13 @@ Global
{53239038-E234-4DEF-B730-953B2F43B51E} = {932FD4A5-FCE7-4428-A0C1-C0392D90A21A}
{CC74DC68-A0A5-40A2-9A4D-CF110685D8C4} = {92C71AA7-E791-40C0-9F3A-2A85880B0439}
{17F36A0B-940E-49DB-B5B5-38C38E0947EE} = {D02161B3-8242-4BF5-96E9-780465A5023B}
{48432457-6A55-4D03-9D40-260CE8E06440} = {2E99F15F-A82A-4734-A837-C0F768702600}
{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA} = {2E99F15F-A82A-4734-A837-C0F768702600}
{326EF526-6200-4570-90DE-5E6D48B63EAC} = {85B157B3-F701-4F75-B4F1-EC2287729480}
{B8CB5C57-07ED-4BC6-ACE8-F05E428E3EB5} = {85B157B3-F701-4F75-B4F1-EC2287729480}
{BDA7536E-26FD-436F-AAC8-F8A2B500548E} = {85B157B3-F701-4F75-B4F1-EC2287729480}
{F5CB3DA2-EB59-4792-A1B3-49F600F7C130} = {85B157B3-F701-4F75-B4F1-EC2287729480}
{48432457-6A55-4D03-9D40-260CE8E06440} = {2E99F15F-A82A-4734-A837-C0F768702600}
{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA} = {2E99F15F-A82A-4734-A837-C0F768702600}
{2B746401-384F-484A-810E-7A65288165E0} = {D02161B3-8242-4BF5-96E9-780465A5023B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C246CAB0-0837-4EE4-A22D-28B3C74930B4}

Просмотреть файл

@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RectPainter.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

Просмотреть файл

@ -0,0 +1,31 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using RectPainter.ViewModels;
namespace RectPainter;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindow = new MainWindow();
var vm = new MainWindowViewModel()
{
Vm = new PaintControlViewModel()
};
mainWindow.DataContext = vm;
desktop.MainWindow = mainWindow;
}
base.OnFrameworkInitializationCompleted();
}
}

Двоичные данные
src/Avalonia.Samples/Drawing/RectPainter/Assets/avalonia-logo.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 172 KiB

Просмотреть файл

@ -0,0 +1,203 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform;
using RectPainter.ViewModels;
namespace RectPainter.Controls
{
/// <summary>
/// A control that responds to mouse and keyboard, to edit and render an image
/// </summary>
public class PaintControl : Control
{
public PaintControlViewModel? Vm => DataContext as PaintControlViewModel;
public PaintControl()
{
// Setup event handlers
PointerMoved += PaintControl_PointerMoved;
PointerPressed += PaintControl_PointerPressed;
PointerReleased += PaintControl_PointerReleased;
PointerCaptureLost += PaintControl_PointerCaptureLost;
SizeChanged += PaintControl_SizeChanged;
KeyDownEvent.AddClassHandler<TopLevel>(PaintControl_KeyDown, handledEventsToo: true);
KeyUpEvent.AddClassHandler<TopLevel>(PaintControl_KeyUp, handledEventsToo: true);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == DataContextProperty && Bounds.Size != default)
{
// let the view model know the PaintControl's size, so the image can be the correct size.
Vm?.SetImageSize(new PixelSize((int)Bounds.Width, (int)Bounds.Height));
// Request the image be rendered
InvalidateVisual();
}
}
private void PaintControl_PointerMoved(object? sender, Avalonia.Input.PointerEventArgs e)
{
if (Vm != null)
{
// Update the mouse position, and the currently selected marquee
var pos = e.GetPosition(this);
Vm.Pos = pos;
if (Vm.Dragging)
{
Vm.Marquee = new Rect(
System.Math.Min(Vm.Origin.X, Vm.Pos.X),
System.Math.Min(Vm.Origin.Y, Vm.Pos.Y),
System.Math.Abs(Vm.Origin.X - Vm.Pos.X),
System.Math.Abs(Vm.Origin.Y - Vm.Pos.Y));
InvalidateVisual();
}
else
{
Vm.Origin = pos;
}
e.Handled = true;
}
}
private void PaintControl_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (Vm != null)
{
// Start the drag
Vm.Dragging = true;
}
}
private void PaintControl_PointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e)
{
if (Vm != null)
{
if (Vm.Dragging == true)
{
// Finish dragging
Vm.Dragging = false;
// Paint a new rectangle
Vm.AddRectangle();
// Request the updated image be rendered
InvalidateVisual();
}
}
}
private void PaintControl_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
if (Vm != null)
{
// finish Dragging
Vm.Dragging = false;
// Request the image be rendered (to clear any marquee)
InvalidateVisual();
}
}
private void PaintControl_SizeChanged(object? sender, SizeChangedEventArgs e)
{
if (Vm != null)
{
// Make sure the image matches the size of the control
Vm.SetImageSize(new PixelSize((int)e.NewSize.Width, (int)e.NewSize.Height));
// Request the updated image be rendered
InvalidateVisual();
}
}
private void PaintControl_KeyDown(object? sender, KeyEventArgs e)
{
if (Vm != null)
{
// Change rectangle color or cancel dragging
// Request the updated image be rendered, in case there is a marquee
switch (e.Key)
{
case Key.LeftShift:
Vm.Red = 255;
InvalidateVisual();
break;
case Key.LeftCtrl:
Vm.Green = 255;
InvalidateVisual();
break;
case Key.LeftAlt:
Vm.Blue = 255;
InvalidateVisual();
break;
case Key.Escape:
Vm.Dragging = false;
InvalidateVisual();
break;
}
}
}
private void PaintControl_KeyUp(object? sender, KeyEventArgs e)
{
if (Vm != null)
{
// Change rectangle color
// Request the updated image be rendered, in case there is a marquee
switch (e.Key)
{
case Key.LeftShift:
Vm.Red = 0;
InvalidateVisual();
break;
case Key.LeftCtrl:
Vm.Green = 0;
InvalidateVisual();
break;
case Key.LeftAlt:
Vm.Blue = 0;
InvalidateVisual();
break;
}
}
}
/// <summary>
/// Render the saved graphic, and marquee when needed
/// </summary>
/// <param name="context"></param>
public override void Render(DrawingContext context)
{
// If there is an image in the view model, copy it to the PaintControl's drawing surface
if (Vm?.Image != null)
{
context.DrawImage(Vm.Image, Bounds);
}
// If we are in a dragging operation, draw a dashed rectangle
// The base color for the rectangle is the color for rectangle that the drag operation will draw
// The alternative color is either black or white to contrast with the base color
if (Vm?.Dragging == true) {
var pen = new Pen(new SolidColorBrush(Color.FromRgb(Vm.Red, Vm.Green, Vm.Blue)));
context.DrawRectangle(pen, Vm.Marquee.Translate(new Vector(0.5, 0.5)));
byte altColor = (byte)(255 - Vm.Green);
pen = new Pen(new SolidColorBrush(Color.FromRgb(altColor, altColor, altColor)), dashStyle: DashStyle.Dash);
context.DrawRectangle(pen, Vm.Marquee.Translate(new Vector(0.5, 0.5)));
}
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
<Window xmlns="https://github.com/avaloniaui"
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:controls="using:RectPainter.Controls"
xmlns:vm="using:RectPainter.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RectPainter.MainWindow"
Title="Rect Painter Sample - Drag out rectangles with the mouse. Use Shift/Ctrl/Alt to change color"
Icon="/Assets/avalonia-logo.ico">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
<TextBlock Width="50" Text="Cursor:"/>
<TextBlock Width="120" Text="{Binding MousePosition}"/>
<TextBlock Width="70" Text="Rectangle:"/>
<TextBlock Width="100" Text="{Binding Rect}"/>
</StackPanel>
<TextBlock Text="Use mouse with Shift/Alt/Ctrl key modifiers pressed."/>
<controls:PaintControl DataContext="{Binding Vm}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</DockPanel>
</Window>

Просмотреть файл

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace RectPainter
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
using System;
using Avalonia;
namespace RectPainter;
internal class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

Просмотреть файл

@ -0,0 +1,47 @@
# Rect Painter Sample
<!-- Write a short summary here what this examples does -->
This example will show you how to create a custom rendered control which interacts with the mouse, to form a simple paint application
### Difficulty
<!-- Choose one of the below difficulties. You can just delete the ones you don't need. -->
🐔 Normal 🐔
### Buzz-Words
<!-- Write some buzz-words here. You can separate them by ", " -->
Graphics Editor, Paint, MVVM
## The Solution
There is a custom control `PaintControl` and a corresponding view model for that control `PaintControlViewModel`.
The view model holds data for the control as properties, and also holds the off-screen image which is being edited.
The `PaintControl` :-
* responds to mouse movement, mouse button presses, and also to global keyboard events
* maintains information about the editing process, what rectangle you are dragging out, and what color it is, in the view model.
* requests the view model to modify the image when you create a new rectangle, or when the image size needs to change in response to the application resizing.
The `PaintControlViewModel` :-
* publishes the properties it holds using the `INotifyPropertyChanged` interface.
* handles the actual editing of the image.
The `MainWindowViewModel` :-
* subscribes to properties in the `PaintControlViewModel` so that it can provide properties to the UI for binding
## Notes
<!-- Any related information or further readings goes here. -->
The `PaintControlViewModel` holds a single image that is being edited, and the `PaintControl` renders that image when needed. Editing the image involves creating a new image, painting the old one onto it, and then adding the newly requested rectangle.
More control and editing capabilities could be acheived using a dedicated graphics library such as Skia.

Просмотреть файл

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<OutputType>WinExe</OutputType>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,106 @@
using System.ComponentModel;
namespace RectPainter.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
private string _mousePosition = string.Empty;
private string _rect = string.Empty;
private PaintControlViewModel? _vm = null;
// A text rendering of the current mouse position
public string MousePosition
{
get => _mousePosition;
set
{
_mousePosition = value;
OnPropertyChanged(nameof(MousePosition));
}
}
// A text rendering of the current marquee dimensions
public string Rect
{
get => _rect;
set
{
_rect = value;
OnPropertyChanged(nameof(Rect));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
// The view model for the PaintControl, is a child of this view model
// just as the PaintControl itself is a child of the MainWindow
public PaintControlViewModel? Vm {
get => _vm;
set
{
if (_vm != value)
{
// Remove event handlers from any previous view model
if (_vm != null)
_vm.PropertyChanged -= _vm_PropertyChanged;
_vm = value;
// Add event handlers for this view model
if (_vm != null)
_vm.PropertyChanged += _vm_PropertyChanged;
}
}
}
private void _vm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(PaintControlViewModel.Dragging):
{
// Update the text for Position and Marquee
SetPos();
SetRect();
break;
}
case nameof(PaintControlViewModel.Pos):
{
// Update the text for Position
SetPos();
break;
}
case nameof(PaintControlViewModel.Marquee):
{
// Update the text for the Marquee
SetRect();
break;
}
}
}
private void SetPos()
{
MousePosition = $"{Vm?.Pos.X} {Vm?.Pos.Y}";
}
private void SetRect()
{
if (Vm?.Dragging == true)
{
Rect = $"{Vm.Marquee.Left} {Vm.Marquee.Top} {Vm.Marquee.Width} {Vm.Marquee.Height}";
}
else
{
Rect = string.Empty;
}
}
}
}

Просмотреть файл

@ -0,0 +1,124 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using System.ComponentModel;
namespace RectPainter.ViewModels
{
public class PaintControlViewModel: INotifyPropertyChanged
{
private bool _dragging = false;
private Point _origin = new Point(0, 0);
private Point _pos = new Point(0, 0);
private Rect _marquee = new Rect();
private RenderTargetBitmap? _image = null;
// Are we in a drag operation?
public bool Dragging
{
get => _dragging;
set
{
if (_dragging != value)
{
_dragging = value;
OnPropertyChanged(nameof(Dragging));
}
}
}
// The current mouse position
public Point Pos
{
get => _pos;
set
{
if (_pos != value)
{
_pos = value;
OnPropertyChanged(nameof(Pos));
}
}
}
// The mouse position at the start of the drag operation
public Point Origin
{
get => _origin;
set
{
if (_origin != value)
{
_origin = value;
OnPropertyChanged(nameof(Origin));
}
}
}
// The drag rectangle
public Rect Marquee
{
get => _marquee;
set
{
if (_marquee != value)
{
_marquee = value;
OnPropertyChanged(nameof(Marquee));
}
}
}
// The color for the rectangle to be drawn
// This can be controlled by pressing Shift, Ctrl and Alt keys
public byte Red { get; set; } = 0;
public byte Green { get; set; } = 0;
public byte Blue { get; set; } = 0;
// The bitmap full of rectangles
public IImage? Image
{
get => _image;
}
// When the control changes, we need to change the bitmap to match
public void SetImageSize(PixelSize size)
{
RenderTargetBitmap newImage = new RenderTargetBitmap(new PixelSize(size.Width, size.Height), new Vector(96, 96));
if (_image != null)
{
// If there was already a bitmap, copy it into the new one.
using (var context = newImage.CreateDrawingContext())
{
context.DrawImage(_image, new Rect(0, 0, size.Width, size.Height), new Rect(0, 0, size.Width, size.Height));
}
}
_image = newImage;
OnPropertyChanged(nameof(Image));
}
public void AddRectangle()
{
if (_image != null)
{
// Create a new image, copy the old one, and then add a new rectangle to it
// using the current marquee and color
RenderTargetBitmap newImage = new RenderTargetBitmap(_image.PixelSize, _image.Dpi);
using (var context = newImage.CreateDrawingContext())
{
context.DrawImage(_image, new Rect(0, 0, _image.PixelSize.Width, _image.PixelSize.Height));
var brush = new SolidColorBrush(Color.FromRgb(Red, Green, Blue));
context.FillRectangle(brush, _marquee);
}
_image.Dispose();
_image = newImage;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}

Двоичные данные
src/Avalonia.Samples/Drawing/RectPainter/demo.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.5 KiB