From 0081ec8959f443698c9418c816613d68ef48c97d Mon Sep 17 00:00:00 2001 From: yaobiao131 <28655758+yaobiao131@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:03:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0QRCode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 1 + src/AtomUI.Controls/AtomUI.Controls.csproj | 1 + .../AtomUIOSSControlThemesProvider.axaml | 1 + src/AtomUI.Controls/QRCode/QRCode.cs | 272 ++++++++++++++++++ src/AtomUI.Controls/QRCode/QRCodeToken.cs | 31 ++ .../QRCode/Themes/QRCodeTheme.axaml | 86 ++++++ 6 files changed, 392 insertions(+) create mode 100644 src/AtomUI.Controls/QRCode/QRCode.cs create mode 100644 src/AtomUI.Controls/QRCode/QRCodeToken.cs create mode 100644 src/AtomUI.Controls/QRCode/Themes/QRCodeTheme.axaml diff --git a/Directory.Packages.props b/Directory.Packages.props index aea3e0c7f..f8369dfbb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/src/AtomUI.Controls/AtomUI.Controls.csproj b/src/AtomUI.Controls/AtomUI.Controls.csproj index 46c6ae2e8..16f9dea4f 100644 --- a/src/AtomUI.Controls/AtomUI.Controls.csproj +++ b/src/AtomUI.Controls/AtomUI.Controls.csproj @@ -16,6 +16,7 @@ + diff --git a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml index 6b125ab63..363ac14ae 100644 --- a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml +++ b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml @@ -43,6 +43,7 @@ + diff --git a/src/AtomUI.Controls/QRCode/QRCode.cs b/src/AtomUI.Controls/QRCode/QRCode.cs new file mode 100644 index 000000000..a42af4b29 --- /dev/null +++ b/src/AtomUI.Controls/QRCode/QRCode.cs @@ -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; + +/// +/// 二维码纠错等级 +/// +public enum QRCodeEccLevel +{ + L = 0, + M = 1, + Q = 2, + H = 3 +} + +/// +/// QRCode状态 +/// +public enum QRCodeStatus +{ + Active = 0, + Expired, + Loading, + Scanned, +} + +public class QRCode : TemplatedControl, + IControlSharedTokenResourcesHost +{ + #region 公共属性定义 + + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value)); + + public static readonly StyledProperty BorderedProperty = + AvaloniaProperty.Register(nameof(Bordered), true); + + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register(nameof(Color), new ImmutableSolidColorBrush(Colors.Black)); + + public static readonly StyledProperty BgColorProperty = + AvaloniaProperty.Register(nameof(BgColor), new ImmutableSolidColorBrush(Colors.Transparent)); + + public static readonly StyledProperty EccLevelProperty = + AvaloniaProperty.Register(nameof(EccLevel), QRCodeEccLevel.M); + + public static readonly StyledProperty SizeProperty = + AvaloniaProperty.Register(nameof(Size), 160); + + public static readonly StyledProperty IconSizeProperty = + AvaloniaProperty.Register(nameof(IconSize), 40); + + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + public static readonly StyledProperty IconBgColorProperty = + AvaloniaProperty.Register(nameof(IconBgColor)); + + public static readonly StyledProperty StatusProperty = + AvaloniaProperty.Register(nameof(Status)); + + public static readonly StyledProperty LoadingContentProperty = + AvaloniaProperty.Register(nameof(LoadingContent)); + + public static readonly StyledProperty LoadingContentTemplateProperty = + AvaloniaProperty.Register(nameof(LoadingContentTemplate)); + + public static readonly StyledProperty ExpiredContentProperty = + AvaloniaProperty.Register(nameof(ExpiredContent)); + + public static readonly StyledProperty ExpiredContentTemplateProperty = + AvaloniaProperty.Register(nameof(ExpiredContentTemplate)); + + public static readonly StyledProperty ScannedContentProperty = + AvaloniaProperty.Register(nameof(ScannedContent)); + + public static readonly StyledProperty ScannedContentTemplateProperty = + AvaloniaProperty.Register(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 BitmapProperty = + AvaloniaProperty.Register(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(); + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/QRCode/QRCodeToken.cs b/src/AtomUI.Controls/QRCode/QRCodeToken.cs new file mode 100644 index 000000000..59c059fa3 --- /dev/null +++ b/src/AtomUI.Controls/QRCode/QRCodeToken.cs @@ -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"; + + /// + /// 按钮内间距 + /// + 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); + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/QRCode/Themes/QRCodeTheme.axaml b/src/AtomUI.Controls/QRCode/Themes/QRCodeTheme.axaml new file mode 100644 index 000000000..be793a66e --- /dev/null +++ b/src/AtomUI.Controls/QRCode/Themes/QRCodeTheme.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file