Merge pull request #113 from yaobiao131/release/5.0

feat: 实现QRCode
This commit is contained in:
qinware
2025-11-04 08:27:05 +08:00
committed by GitHub
6 changed files with 392 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@
<!-- TODO 版本是否跟随 AvaloniaVersion?-->
<PackageVersion Include="Avalonia.Svg" Version="11.3.0"/>
<PackageVersion Include="Avalonia.Controls.TreeDataGrid" Version="11.1.1"/>
<PackageVersion Include="SkiaSharp.QrCode" Version="0.7.0" />
<PackageVersion Include="System.Reactive" Version="6.0.1"/>
<!-- 测试相关依赖 -->
@@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Svg" />
<PackageReference Include="SkiaSharp.QrCode" />
</ItemGroup>
<ItemGroup>
@@ -43,6 +43,7 @@
<ResourceInclude Source="PopupConfirm/Themes/PopupConfirmThemes.axaml" />
<ResourceInclude Source="Primitives/Themes/PrimitiveThemes.axaml" />
<ResourceInclude Source="ProgressBar/Themes/ProgressBarThemes.axaml" />
<ResourceInclude Source="QRCode/Themes/QRCodeTheme.axaml" />
<ResourceInclude Source="RadioButton/Themes/RadioButtonThemes.axaml" />
<ResourceInclude Source="ScrollBar/Themes/ScrollBarThemes.axaml" />
<ResourceInclude Source="Segmented/Themes/SegmentedThemes.axaml" />
+272
View File
@@ -0,0 +1,272 @@
using System.Reactive.Disposables;
using AtomUI.Theme;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using SkiaSharp;
using SkiaSharp.QrCode;
using static System.Enum;
namespace AtomUI.Controls;
/// <summary>
/// 二维码纠错等级
/// </summary>
public enum QRCodeEccLevel
{
L = 0,
M = 1,
Q = 2,
H = 3
}
/// <summary>
/// QRCode状态
/// </summary>
public enum QRCodeStatus
{
Active = 0,
Expired,
Loading,
Scanned,
}
public class QRCode : TemplatedControl,
IControlSharedTokenResourcesHost
{
#region 公共属性定义
public static readonly StyledProperty<string> ValueProperty =
AvaloniaProperty.Register<QRCode, string>(nameof(Value));
public static readonly StyledProperty<bool> BorderedProperty =
AvaloniaProperty.Register<QRCode, bool>(nameof(Bordered), true);
public static readonly StyledProperty<IBrush> ColorProperty =
AvaloniaProperty.Register<QRCode, IBrush>(nameof(Color), new ImmutableSolidColorBrush(Colors.Black));
public static readonly StyledProperty<IBrush> BgColorProperty =
AvaloniaProperty.Register<QRCode, IBrush>(nameof(BgColor), new ImmutableSolidColorBrush(Colors.Transparent));
public static readonly StyledProperty<QRCodeEccLevel> EccLevelProperty =
AvaloniaProperty.Register<QRCode, QRCodeEccLevel>(nameof(EccLevel), QRCodeEccLevel.M);
public static readonly StyledProperty<int> SizeProperty =
AvaloniaProperty.Register<QRCode, int>(nameof(Size), 160);
public static readonly StyledProperty<int> IconSizeProperty =
AvaloniaProperty.Register<QRCode, int>(nameof(IconSize), 40);
public static readonly StyledProperty<IImage?> IconProperty =
AvaloniaProperty.Register<QRCode, IImage?>(nameof(Icon));
public static readonly StyledProperty<IBrush?> IconBgColorProperty =
AvaloniaProperty.Register<QRCode, IBrush?>(nameof(IconBgColor));
public static readonly StyledProperty<QRCodeStatus> StatusProperty =
AvaloniaProperty.Register<QRCode, QRCodeStatus>(nameof(Status));
public static readonly StyledProperty<object?> LoadingContentProperty =
AvaloniaProperty.Register<QRCode, object?>(nameof(LoadingContent));
public static readonly StyledProperty<IDataTemplate?> LoadingContentTemplateProperty =
AvaloniaProperty.Register<QRCode, IDataTemplate?>(nameof(LoadingContentTemplate));
public static readonly StyledProperty<object?> ExpiredContentProperty =
AvaloniaProperty.Register<QRCode, object?>(nameof(ExpiredContent));
public static readonly StyledProperty<IDataTemplate?> ExpiredContentTemplateProperty =
AvaloniaProperty.Register<QRCode, IDataTemplate?>(nameof(ExpiredContentTemplate));
public static readonly StyledProperty<object?> ScannedContentProperty =
AvaloniaProperty.Register<QRCode, object?>(nameof(ScannedContent));
public static readonly StyledProperty<IDataTemplate?> ScannedContentTemplateProperty =
AvaloniaProperty.Register<QRCode, IDataTemplate?>(nameof(ScannedContentTemplate));
public string Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public bool Bordered
{
get => GetValue(BorderedProperty);
set => SetValue(BorderedProperty, value);
}
public IBrush Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public IBrush BgColor
{
get => GetValue(BgColorProperty);
set => SetValue(BgColorProperty, value);
}
public int Size
{
get => GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
public QRCodeEccLevel EccLevel
{
get => GetValue(EccLevelProperty);
set => SetValue(EccLevelProperty, value);
}
public int IconSize
{
get => GetValue(IconSizeProperty);
set => SetValue(IconSizeProperty, value);
}
public IImage? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public IBrush? IconBgColor
{
get => GetValue(IconBgColorProperty);
set => SetValue(IconBgColorProperty, value);
}
public QRCodeStatus Status
{
get => GetValue(StatusProperty);
set => SetValue(StatusProperty, value);
}
public object? LoadingContent
{
get => GetValue(LoadingContentProperty);
set => SetValue(LoadingContentProperty, value);
}
public IDataTemplate? LoadingContentTemplate
{
get => GetValue(LoadingContentTemplateProperty);
set => SetValue(LoadingContentTemplateProperty, value);
}
public object? ExpiredContent
{
get => GetValue(ExpiredContentProperty);
set => SetValue(ExpiredContentProperty, value);
}
public IDataTemplate? ExpiredContentTemplate
{
get => GetValue(ExpiredContentTemplateProperty);
set => SetValue(ExpiredContentTemplateProperty, value);
}
public object? ScannedContent
{
get => GetValue(ScannedContentProperty);
set => SetValue(ScannedContentProperty, value);
}
public IDataTemplate? ScannedContentTemplate
{
get => GetValue(ScannedContentTemplateProperty);
set => SetValue(ScannedContentTemplateProperty, value);
}
#endregion
#region 内部属性定义
internal static readonly StyledProperty<Bitmap> BitmapProperty =
AvaloniaProperty.Register<QRCode, Bitmap>(nameof(Bitmap));
internal Bitmap Bitmap
{
get => GetValue(BitmapProperty);
set => SetValue(BitmapProperty, value);
}
#endregion
Control IControlSharedTokenResourcesHost.HostControl => this;
string IControlSharedTokenResourcesHost.TokenId => OptionButtonToken.ID;
private Button? _refreshButton;
private CompositeDisposable? _disposables;
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_disposables?.Dispose();
_disposables = new CompositeDisposable(2);
_disposables?.Add(TokenResourceBinder.CreateTokenBinding(this, ColorProperty, SharedTokenKey.ColorText));
if (IconBgColor == null)
_disposables?.Add(TokenResourceBinder.CreateTokenBinding(this, IconBgColorProperty, SharedTokenKey.ColorBgContainer));
SetupQRCode();
}
private void SetupQRCode()
{
if (Value == null) return;
TryParse(EccLevel.ToString(), out ECCLevel eccLevel);
using var generator = new QRCodeGenerator();
var qrcode = generator.CreateQrCode(Value, eccLevel, quietZoneSize: 0);
var info = new SKImageInfo(Size, Size);
using var surface = SKSurface.Create(info);
var canvas = surface.Canvas;
var color = ((ISolidColorBrush)Color).Color;
var bgColor = ((ISolidColorBrush)BgColor).Color;
canvas.Render(
qrcode,
info.Width,
info.Height,
SKColor.Empty,
new SKColor(color.R, color.G, color.B, color.A),
new SKColor(bgColor.R, bgColor.G, bgColor.B, bgColor.A)
);
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
Bitmap = new Bitmap(data.AsStream());
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ValueProperty || e.Property == ColorProperty || e.Property == BgColorProperty || e.Property == EccLevelProperty || e.Property == SizeProperty)
{
SetupQRCode();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_disposables?.Dispose();
}
}
+31
View File
@@ -0,0 +1,31 @@
using AtomUI.Theme.TokenSystem;
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
[ControlDesignToken]
internal class QRCodeToken : AbstractControlDesignToken
{
public const string ID = "QRCode";
/// <summary>
/// 按钮内间距
/// </summary>
public Thickness Border { get; set; }
public Color QRCodeMaskBackgroundColor { get; set; }
public QRCodeToken()
: base(ID)
{
}
public override void CalculateFromAlias()
{
base.CalculateFromAlias();
Border = new Thickness(SharedToken.LineWidth);
var colorBgContainer = SharedToken.ColorBgContainer;
QRCodeMaskBackgroundColor = Color.FromArgb(244, colorBgContainer.R, colorBgContainer.G, colorBgContainer.B);
}
}
@@ -0,0 +1,86 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:atom="https://atomui.net">
<ControlTheme x:Key="{x:Type atom:QRCode}" TargetType="atom:QRCode">
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_Border"
Width="{TemplateBinding Size}"
Height="{TemplateBinding Size}"
Background="{TemplateBinding BgColor}"
Padding="{DynamicResource {x:Static atom:SharedTokenKey.PaddingSM}}"
CornerRadius="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadiusLG}}">
<Panel>
<Panel>
<Image Source="{TemplateBinding Bitmap}" />
<Border Width="{TemplateBinding IconSize}"
Height="{TemplateBinding IconSize}"
Background="{TemplateBinding IconBgColor}"
IsVisible="{TemplateBinding Icon, Converter={x:Static ObjectConverters.IsNotNull}}">
<Image
Source="{TemplateBinding Icon}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Panel>
<Border Name="PART_Mask"
Background="{DynamicResource {x:Static atom:QRCodeTokenKey.QRCodeMaskBackgroundColor}}"
IsVisible="False" />
<ContentPresenter Name="PART_Status"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[Bordered=True]">
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="BorderThickness" Value="{DynamicResource {x:Static atom:QRCodeTokenKey.Border}}" />
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorSplit}}" />
</Style>
</Style>
<Style Selector="^[Status=Active]">
<Style Selector="^ /template/ Border#PART_Mask">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_Status">
<Setter Property="IsVisible" Value="False" />
<Setter Property="Content" Value="{x:Null}" />
<Setter Property="ContentTemplate" Value="{x:Null}" />
</Style>
</Style>
<Style Selector="^[Status=Loading]">
<Style Selector="^ /template/ Border#PART_Mask">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_Status">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Content" Value="{TemplateBinding LoadingContent}" />
<Setter Property="ContentTemplate" Value="{TemplateBinding LoadingContentTemplate}" />
</Style>
</Style>
<Style Selector="^[Status=Expired]">
<Style Selector="^ /template/ Border#PART_Mask">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_Status">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Content" Value="{TemplateBinding ExpiredContent}" />
<Setter Property="ContentTemplate" Value="{TemplateBinding ExpiredContentTemplate}" />
</Style>
</Style>
<Style Selector="^[Status=Scanned]">
<Style Selector="^ /template/ Border#PART_Mask">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_Status">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Content" Value="{TemplateBinding ScannedContent}" />
<Setter Property="ContentTemplate" Value="{TemplateBinding ScannedContentTemplate}" />
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>