Docs added

This commit is contained in:
2026-01-17 05:27:21 +00:00
parent 7d2ac327a3
commit ad12779b73
3 changed files with 1291 additions and 298 deletions

View File

File diff suppressed because it is too large Load Diff

689
docs/CUSTOM_CONTROLS.md Normal file
View File

@@ -0,0 +1,689 @@
# Creating Custom Controls for OpenMaui Linux
This guide explains how to create custom controls that integrate with the OpenMaui Linux platform.
## Overview
OpenMaui Linux uses a layered architecture:
1. **MAUI Virtual Views** - Standard .NET MAUI controls (Button, Label, etc.)
2. **Handlers** - Bridge between MAUI and platform views
3. **Platform Views** - SkiaSharp-rendered controls
When creating custom controls, you can either:
- Create a MAUI control with a custom handler (recommended for reusable controls)
- Create a platform-specific SkiaView directly (for Linux-only functionality)
## Creating a MAUI Control with Handler
### Step 1: Define the MAUI Control
Create a standard MAUI control that inherits from `View`:
```csharp
// Controls/RatingControl.cs
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace MyApp.Controls;
public class RatingControl : View
{
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(
nameof(Value),
typeof(int),
typeof(RatingControl),
0,
BindingMode.TwoWay);
public static readonly BindableProperty MaxValueProperty =
BindableProperty.Create(
nameof(MaxValue),
typeof(int),
typeof(RatingControl),
5);
public static readonly BindableProperty StarColorProperty =
BindableProperty.Create(
nameof(StarColor),
typeof(Color),
typeof(RatingControl),
Colors.Gold);
public static readonly BindableProperty EmptyStarColorProperty =
BindableProperty.Create(
nameof(EmptyStarColor),
typeof(Color),
typeof(RatingControl),
Colors.Gray);
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public int MaxValue
{
get => (int)GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
public Color StarColor
{
get => (Color)GetValue(StarColorProperty);
set => SetValue(StarColorProperty, value);
}
public Color EmptyStarColor
{
get => (Color)GetValue(EmptyStarColorProperty);
set => SetValue(EmptyStarColorProperty, value);
}
public event EventHandler<int>? ValueChanged;
internal void SendValueChanged(int value)
{
ValueChanged?.Invoke(this, value);
}
}
```
### Step 2: Create the Platform View
Create a SkiaView that renders your control:
```csharp
// Platforms/Linux/SkiaRatingControl.cs
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using SkiaSharp;
namespace MyApp.Platforms.Linux;
public class SkiaRatingControl : SkiaView
{
private int _value;
private int _maxValue = 5;
private Color _starColor = Colors.Gold;
private Color _emptyStarColor = Colors.Gray;
private const float StarSize = 24f;
private const float StarSpacing = 4f;
public int Value
{
get => _value;
set
{
if (_value != value)
{
_value = Math.Clamp(value, 0, MaxValue);
Invalidate();
}
}
}
public int MaxValue
{
get => _maxValue;
set
{
if (_maxValue != value)
{
_maxValue = Math.Max(1, value);
InvalidateMeasure();
}
}
}
public Color StarColor
{
get => _starColor;
set
{
if (_starColor != value)
{
_starColor = value;
Invalidate();
}
}
}
public Color EmptyStarColor
{
get => _emptyStarColor;
set
{
if (_emptyStarColor != value)
{
_emptyStarColor = value;
Invalidate();
}
}
}
public event EventHandler<int>? ValueChanged;
protected override Size MeasureOverride(Size availableSize)
{
var width = MaxValue * StarSize + (MaxValue - 1) * StarSpacing;
return new Size(width, StarSize);
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
for (int i = 0; i < MaxValue; i++)
{
var x = bounds.Left + i * (StarSize + StarSpacing);
var y = bounds.Top;
var color = i < Value ? StarColor : EmptyStarColor;
DrawStar(canvas, x, y, StarSize, color);
}
}
private void DrawStar(SKCanvas canvas, float x, float y, float size, Color color)
{
using var paint = new SKPaint
{
Color = color.ToSKColor(),
IsAntialias = true,
Style = SKPaintStyle.Fill
};
var path = CreateStarPath(x + size / 2, y + size / 2, size / 2, size / 4);
canvas.DrawPath(path, paint);
}
private SKPath CreateStarPath(float cx, float cy, float outerRadius, float innerRadius)
{
var path = new SKPath();
var angle = -Math.PI / 2;
var step = Math.PI / 5;
path.MoveTo(
cx + (float)(outerRadius * Math.Cos(angle)),
cy + (float)(outerRadius * Math.Sin(angle)));
for (int i = 0; i < 10; i++)
{
angle += step;
var radius = i % 2 == 0 ? innerRadius : outerRadius;
path.LineTo(
cx + (float)(radius * Math.Cos(angle)),
cy + (float)(radius * Math.Sin(angle)));
}
path.Close();
return path;
}
public override void OnPointerPressed(PointerEventArgs e)
{
if (!IsEnabled) return;
var starIndex = (int)((e.X - Bounds.Left) / (StarSize + StarSpacing));
var newValue = Math.Clamp(starIndex + 1, 0, MaxValue);
if (newValue != Value)
{
Value = newValue;
ValueChanged?.Invoke(this, Value);
}
e.Handled = true;
}
}
```
### Step 3: Create the Handler
Connect the MAUI control to the platform view:
```csharp
// Handlers/RatingControlHandler.cs
using Microsoft.Maui.Handlers;
using MyApp.Controls;
using MyApp.Platforms.Linux;
namespace MyApp.Handlers;
public partial class RatingControlHandler : ViewHandler<RatingControl, SkiaRatingControl>
{
public static IPropertyMapper<RatingControl, RatingControlHandler> PropertyMapper =
new PropertyMapper<RatingControl, RatingControlHandler>(ViewMapper)
{
[nameof(RatingControl.Value)] = MapValue,
[nameof(RatingControl.MaxValue)] = MapMaxValue,
[nameof(RatingControl.StarColor)] = MapStarColor,
[nameof(RatingControl.EmptyStarColor)] = MapEmptyStarColor,
};
public static CommandMapper<RatingControl, RatingControlHandler> CommandMapper =
new(ViewCommandMapper);
public RatingControlHandler() : base(PropertyMapper, CommandMapper)
{
}
protected override SkiaRatingControl CreatePlatformView()
{
return new SkiaRatingControl();
}
protected override void ConnectHandler(SkiaRatingControl platformView)
{
base.ConnectHandler(platformView);
platformView.ValueChanged += OnPlatformValueChanged;
}
protected override void DisconnectHandler(SkiaRatingControl platformView)
{
platformView.ValueChanged -= OnPlatformValueChanged;
base.DisconnectHandler(platformView);
}
private void OnPlatformValueChanged(object? sender, int value)
{
VirtualView.Value = value;
VirtualView.SendValueChanged(value);
}
private static void MapValue(RatingControlHandler handler, RatingControl control)
{
handler.PlatformView.Value = control.Value;
}
private static void MapMaxValue(RatingControlHandler handler, RatingControl control)
{
handler.PlatformView.MaxValue = control.MaxValue;
}
private static void MapStarColor(RatingControlHandler handler, RatingControl control)
{
handler.PlatformView.StarColor = control.StarColor;
}
private static void MapEmptyStarColor(RatingControlHandler handler, RatingControl control)
{
handler.PlatformView.EmptyStarColor = control.EmptyStarColor;
}
}
```
### Step 4: Register the Handler
Register your handler in `MauiProgram.cs`:
```csharp
using Microsoft.Maui.Hosting;
using MyApp.Controls;
using MyApp.Handlers;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseOpenMauiLinux()
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<RatingControl, RatingControlHandler>();
});
return builder.Build();
}
}
```
### Step 5: Use in XAML
```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:MyApp.Controls"
x:Class="MyApp.MainPage">
<VerticalStackLayout>
<Label Text="Rate this product:" />
<controls:RatingControl
Value="{Binding Rating}"
MaxValue="5"
StarColor="Gold"
EmptyStarColor="LightGray"
ValueChanged="OnRatingChanged" />
</VerticalStackLayout>
</ContentPage>
```
## Creating a Direct SkiaView
For Linux-only controls or simpler use cases, inherit directly from `SkiaView`:
```csharp
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using SkiaSharp;
public class CustomGauge : SkiaView
{
private double _value;
private double _minimum;
private double _maximum = 100;
public double Value
{
get => _value;
set
{
_value = Math.Clamp(value, Minimum, Maximum);
Invalidate();
}
}
public double Minimum
{
get => _minimum;
set { _minimum = value; Invalidate(); }
}
public double Maximum
{
get => _maximum;
set { _maximum = value; Invalidate(); }
}
protected override Size MeasureOverride(Size availableSize)
{
return new Size(100, 100); // Fixed size gauge
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var center = new SKPoint(bounds.MidX, bounds.MidY);
var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 10;
// Draw background arc
using var bgPaint = new SKPaint
{
Color = SKColors.LightGray,
Style = SKPaintStyle.Stroke,
StrokeWidth = 10,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
canvas.DrawArc(
new SKRect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius),
135, 270, false, bgPaint);
// Draw value arc
var percentage = (Value - Minimum) / (Maximum - Minimum);
var sweepAngle = (float)(270 * percentage);
using var valuePaint = new SKPaint
{
Color = SKColors.Blue,
Style = SKPaintStyle.Stroke,
StrokeWidth = 10,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
canvas.DrawArc(
new SKRect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius),
135, sweepAngle, false, valuePaint);
// Draw value text
using var textPaint = new SKPaint
{
Color = SKColors.Black,
TextSize = 24,
TextAlign = SKTextAlign.Center,
IsAntialias = true
};
canvas.DrawText(
$"{Value:F0}",
center.X, center.Y + 8,
textPaint);
}
}
```
## Best Practices
### 1. Use MAUI Types for Public APIs
Always use MAUI types (Color, Rect, Size, Thickness, double) in public APIs:
```csharp
// Good - MAUI types
public Color ForegroundColor { get; set; }
public double BorderWidth { get; set; }
public Thickness Padding { get; set; }
// Bad - SkiaSharp types (internal only)
// public SKColor ForegroundColor { get; set; } // Don't expose
// public float BorderWidth { get; set; } // Don't expose
```
### 2. Implement Visual States
Support visual state changes for interactive controls:
```csharp
public override void OnPointerEntered(PointerEventArgs e)
{
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver);
base.OnPointerEntered(e);
}
public override void OnPointerExited(PointerEventArgs e)
{
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal);
base.OnPointerExited(e);
}
public override void OnPointerPressed(PointerEventArgs e)
{
SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed);
base.OnPointerPressed(e);
}
```
### 3. Support Accessibility
Override accessibility methods for screen reader support:
```csharp
protected override string GetDefaultAccessibleName()
{
return $"Rating: {Value} of {MaxValue} stars";
}
protected override AccessibleRole GetAccessibleRole()
{
return AccessibleRole.Slider;
}
protected override IReadOnlyList<AccessibleAction> GetAccessibleActions()
{
return new[]
{
new AccessibleAction("Increment", "Increase rating"),
new AccessibleAction("Decrement", "Decrease rating")
};
}
protected override bool DoAccessibleAction(string actionName)
{
switch (actionName)
{
case "Increment":
Value = Math.Min(Value + 1, MaxValue);
return true;
case "Decrement":
Value = Math.Max(Value - 1, 0);
return true;
}
return false;
}
```
### 4. Implement Keyboard Navigation
Support keyboard input for accessibility:
```csharp
public CustomControl()
{
IsFocusable = true;
}
public override void OnKeyDown(KeyEventArgs e)
{
if (!IsEnabled) return;
switch (e.Key)
{
case Key.Left:
case Key.Down:
Value--;
e.Handled = true;
break;
case Key.Right:
case Key.Up:
Value++;
e.Handled = true;
break;
case Key.Home:
Value = Minimum;
e.Handled = true;
break;
case Key.End:
Value = Maximum;
e.Handled = true;
break;
}
}
```
### 5. Handle Focus Visuals
Draw focus indicators when the control is focused:
```csharp
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
// Draw control content...
// Draw focus ring
if (IsFocused)
{
using var focusPaint = new SKPaint
{
Color = SkiaTheme.PrimarySK.WithAlpha(100),
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawRoundRect(bounds.Inflate(2, 2), 4, 4, focusPaint);
}
}
```
### 6. Optimize Rendering
Use `Invalidate()` sparingly and implement dirty region tracking:
```csharp
// Only invalidate what changed
public int Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
Invalidate(); // Request redraw
}
}
}
// Use InvalidateMeasure() when size changes
public int MaxValue
{
get => _maxValue;
set
{
if (_maxValue != value)
{
_maxValue = value;
InvalidateMeasure(); // Size changed, remeasure needed
}
}
}
```
## Testing Custom Controls
Create unit tests for your controls:
```csharp
using FluentAssertions;
using Microsoft.Maui.Graphics;
using Xunit;
public class SkiaRatingControlTests
{
[Fact]
public void Value_ClampedToMaxValue()
{
var control = new SkiaRatingControl { MaxValue = 5 };
control.Value = 10;
control.Value.Should().Be(5);
}
[Fact]
public void Value_ClampedToZero()
{
var control = new SkiaRatingControl();
control.Value = -5;
control.Value.Should().Be(0);
}
[Fact]
public void Measure_ReturnsCorrectSize()
{
var control = new SkiaRatingControl { MaxValue = 5 };
var size = control.Measure(new Size(500, 500));
size.Width.Should().BeGreaterThan(0);
size.Height.Should().Be(24); // StarSize
}
}
```
## Summary
Creating custom controls for OpenMaui Linux follows these patterns:
1. **Define MAUI control** with BindableProperties and events
2. **Create platform view** inheriting from SkiaView
3. **Create handler** to connect MAUI and platform views
4. **Register handler** in MauiProgram.cs
5. **Follow best practices** for accessibility, visual states, and performance
For more examples, see the existing controls in the `Views/` directory.

View File

@@ -97,51 +97,56 @@ MyApp/
## Basic Controls
All controls use standard .NET MAUI types (Color, Rect, Size, Thickness) for full API compliance.
### Labels
```csharp
var label = new SkiaLabel
using Microsoft.Maui.Graphics;
var label = new Label
{
Text = "Hello World",
TextColor = new SKColor(33, 33, 33),
FontSize = 16f
TextColor = Color.FromRgb(33, 33, 33), // MAUI Color
FontSize = 16
};
```
### Buttons
```csharp
var button = new SkiaButton
var button = new Button
{
Text = "Click Me",
BackgroundColor = new SKColor(33, 150, 243)
BackgroundColor = Color.FromRgb(33, 150, 243) // MAUI Color
};
button.Clicked += (s, e) => Console.WriteLine("Clicked!");
```
### Text Input
```csharp
var entry = new SkiaEntry
var entry = new Entry
{
Placeholder = "Enter text...",
MaxLength = 100
};
entry.TextChanged += (s, e) => Console.WriteLine($"Text: {e.NewValue}");
entry.TextChanged += (s, e) => Console.WriteLine($"Text: {e.NewTextValue}");
```
### Layouts
```csharp
// Vertical stack
var vstack = new SkiaStackLayout
var vstack = new VerticalStackLayout
{
Orientation = StackOrientation.Vertical,
Spacing = 10
Spacing = 10,
Children =
{
new Label { Text = "Item 1" },
new Label { Text = "Item 2" }
}
};
vstack.AddChild(new SkiaLabel { Text = "Item 1" });
vstack.AddChild(new SkiaLabel { Text = "Item 2" });
// Horizontal stack
var hstack = new SkiaStackLayout
var hstack = new HorizontalStackLayout
{
Orientation = StackOrientation.Horizontal,
Spacing = 8
};
```
@@ -150,24 +155,28 @@ var hstack = new SkiaStackLayout
### CarouselView
```csharp
var carousel = new SkiaCarouselView
var carousel = new CarouselView
{
Loop = true,
PeekAreaInsets = 20f,
ShowIndicators = true
PeekAreaInsets = new Thickness(20),
ItemsSource = new[] { "Page 1", "Page 2", "Page 3" },
ItemTemplate = new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, ".");
return label;
})
};
carousel.AddItem(new SkiaLabel { Text = "Page 1" });
carousel.AddItem(new SkiaLabel { Text = "Page 2" });
carousel.PositionChanged += (s, e) =>
Console.WriteLine($"Position: {e.CurrentPosition}");
```
### RefreshView
```csharp
var refreshView = new SkiaRefreshView
var refreshView = new RefreshView
{
Content = myScrollableContent,
RefreshColor = SKColors.Blue
RefreshColor = Colors.Blue // MAUI Color
};
refreshView.Refreshing += async (s, e) =>
{
@@ -178,26 +187,29 @@ refreshView.Refreshing += async (s, e) =>
### SwipeView
```csharp
var swipeView = new SkiaSwipeView
var swipeView = new SwipeView
{
Content = new SkiaLabel { Text = "Swipe me" }
Content = new Label { Text = "Swipe me" }
};
swipeView.RightItems.Add(new SwipeItem
swipeView.RightItems = new SwipeItems
{
Text = "Delete",
BackgroundColor = SKColors.Red
});
new SwipeItem
{
Text = "Delete",
BackgroundColor = Colors.Red // MAUI Color
}
};
```
### MenuBar
```csharp
var menuBar = new SkiaMenuBar();
var menuBar = new MenuBar();
var fileMenu = new MenuBarItem { Text = "File" };
fileMenu.Items.Add(new MenuItem { Text = "New", Shortcut = "Ctrl+N" });
fileMenu.Items.Add(new MenuItem { Text = "Open", Shortcut = "Ctrl+O" });
fileMenu.Items.Add(new MenuItem { IsSeparator = true });
fileMenu.Items.Add(new MenuItem { Text = "Exit" });
menuBar.Items.Add(fileMenu);
fileMenu.Add(new MenuFlyoutItem { Text = "New", KeyboardAccelerators = { new KeyboardAccelerator { Modifiers = KeyboardAcceleratorModifiers.Ctrl, Key = "N" } } });
fileMenu.Add(new MenuFlyoutItem { Text = "Open" });
fileMenu.Add(new MenuFlyoutSeparator());
fileMenu.Add(new MenuFlyoutItem { Text = "Exit" });
menuBar.Add(fileMenu);
```
## Platform Services