feat: Add Card control and card demo with album images.

This commit is contained in:
NaBian committed 2026-06-13 17:15:54 +08:00
1 parent e7005d09b5
commit d6e16736e4
23 files changed
+287 -47

No files matched your search

@@ -0,0 +1,10 @@
namespace HandyControlDemo.Data;
public class CardModel
{
public string? Header { get; set; }
public string? Content { get; set; }
public string? Footer { get; set; }
}
@@ -20,6 +20,7 @@
<None Remove="Resources\Img\cloud.png" />
<None Remove="Resources\Img\icon.ico" />
<None Remove="Resources\Img\Avatar\*.png" />
<None Remove="Resources\Img\Album\*.jpg" />
</ItemGroup>
<ItemGroup>
@@ -27,6 +28,7 @@
<AvaloniaResource Include="Resources\Img\cloud.png" />
<AvaloniaResource Include="Resources\Img\icon.ico" />
<AvaloniaResource Include="Resources\Img\Avatar\*.png" />
<AvaloniaResource Include="Resources\Img\Album\*.jpg" />
</ItemGroup>
<ItemGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -12,6 +13,31 @@ namespace HandyControlDemo.Service;
public class DataService
{
internal ObservableCollection<CardModel> GetCardDataList()
{
return
[
new CardModel { Header = "Atomic", Content = "avares://HandyControlDemo/Resources/Img/Album/1.jpg", Footer = "Stive Morgan" },
new CardModel { Header = "Zinderlong", Content = "avares://HandyControlDemo/Resources/Img/Album/2.jpg", Footer = "Zonderling" },
new CardModel { Header = "Busy Doin' Nothin'", Content = "avares://HandyControlDemo/Resources/Img/Album/3.jpg", Footer = "Ace Wilder" },
new CardModel { Header = "Wrong", Content = "avares://HandyControlDemo/Resources/Img/Album/4.jpg", Footer = "Blaxy Girls" },
new CardModel { Header = "The Lights", Content = "avares://HandyControlDemo/Resources/Img/Album/5.jpg", Footer = "Panda Eyes" },
new CardModel { Header = "EA7-50-Cent Disco", Content = "avares://HandyControlDemo/Resources/Img/Album/6.jpg", Footer = "еяхат музыка" },
new CardModel { Header = "Monsters", Content = "avares://HandyControlDemo/Resources/Img/Album/7.jpg", Footer = "Different Heaven" },
new CardModel { Header = "Gangsta Walk", Content = "avares://HandyControlDemo/Resources/Img/Album/8.jpg", Footer = "Illusionize" },
new CardModel { Header = "Won't Back Down", Content = "avares://HandyControlDemo/Resources/Img/Album/9.jpg", Footer = "Boehm / Benjamin Francis Leftwich" },
new CardModel { Header = "Katchi", Content = "avares://HandyControlDemo/Resources/Img/Album/10.jpg", Footer = "Ofenbach / Nick Waterhouse" }
];
}
internal CardModel GetCardData()
{
return new CardModel
{
Content = $"avares://HandyControlDemo/Resources/Img/Album/{DateTime.Now.Second % 10 + 1}.jpg"
};
}
internal List<DemoDataModel> GetDemoDataList()
{
var list = new List<DemoDataModel>();
@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace HandyControlDemo.Tools.Converter;
public class StringToBitmapConverter : IValueConverter
{
public static readonly StringToBitmapConverter Instance = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string path || string.IsNullOrEmpty(path))
{
return null;
}
try
{
using var stream = AssetLoader.Open(new Uri(path));
return new Bitmap(stream);
}
catch
{
return null;
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
@@ -0,0 +1,53 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
xmlns:data="clr-namespace:HandyControlDemo.Data"
xmlns:cv="clr-namespace:HandyControlDemo.Tools.Converter"
x:Class="HandyControlDemo.UserControl.CardDemo"
x:DataType="vm:CardDemoViewModel"
DataContext="{Binding CardDemo, Source={x:Static vm:ViewModelLocator.Instance}}">
<UserControl.Resources>
<cv:StringToBitmapConverter x:Key="StringToBitmap" />
</UserControl.Resources>
<ScrollViewer>
<ItemsControl Margin="32"
ItemsSource="{Binding DataList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="data:CardModel">
<hc:Card MaxWidth="240"
BorderThickness="0"
Effect="{StaticResource EffectShadow2}"
Margin="8">
<hc:Card.Footer>
<StackPanel Margin="10">
<TextBlock TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
Theme="{StaticResource TextBlockLargeBold}"
Text="{Binding Header}"
HorizontalAlignment="Left" />
<TextBlock TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
Text="{Binding Footer}"
Margin="0,6,0,0"
HorizontalAlignment="Left" />
</StackPanel>
</hc:Card.Footer>
<Border CornerRadius="4,4,0,0"
ClipToBounds="True">
<Image Width="240"
Height="240"
Source="{Binding Content, Converter={StaticResource StringToBitmap}}"
Stretch="Uniform" />
</Border>
</hc:Card>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class CardDemo : Avalonia.Controls.UserControl
{
public CardDemo()
{
InitializeComponent();
}
}
@@ -2,8 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
xmlns:data="clr-namespace:HandyControlDemo.Data"
xmlns:cv="clr-namespace:HandyControlDemo.Tools.Converter"
x:Class="HandyControlDemo.UserControl.HoneycombPanelDemo"
DataContext="{Binding HoneycombPanelDemo, Source={x:Static vm:ViewModelLocator.Instance}}">
x:DataType="vm:CardDemoViewModel"
DataContext="{Binding CardDemo, Source={x:Static vm:ViewModelLocator.Instance}}">
<UserControl.Resources>
<cv:StringToBitmapConverter x:Key="StringToBitmap" />
</UserControl.Resources>
<Grid Margin="32">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -25,13 +31,14 @@
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate DataType="data:CardModel">
<Border Width="60"
Height="60"
Margin="6"
CornerRadius="30"
ClipToBounds="True">
<Image Source="{Binding}" Stretch="UniformToFill" />
<Image Source="{Binding Content, Converter={StaticResource StringToBitmap}}"
Stretch="UniformToFill" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
@@ -0,0 +1,28 @@
using CommunityToolkit.Mvvm.Input;
using HandyControlDemo.Data;
using HandyControlDemo.Service;
namespace HandyControlDemo.ViewModel;
public partial class CardDemoViewModel : DemoViewModelBase<CardModel>
{
private readonly DataService _dataService;
public CardDemoViewModel(DataService dataService)
{
_dataService = dataService;
DataList = dataService.GetCardDataList();
}
[RelayCommand]
private void AddItem() => DataList.Insert(0, _dataService.GetCardData());
[RelayCommand]
private void RemoveItem()
{
if (DataList.Count > 0)
{
DataList.RemoveAt(0);
}
}
}
@@ -1,42 +0,0 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace HandyControlDemo.ViewModel;
public partial class HoneycombPanelDemoViewModel : ObservableObject
{
private static readonly Random Random = new();
[ObservableProperty] private ObservableCollection<Bitmap> _dataList = [];
public HoneycombPanelDemoViewModel()
{
for (int i = 0; i < 7; i++)
{
DataList.Add(LoadRandomAvatar());
}
}
[RelayCommand]
private void AddItem() => DataList.Insert(0, LoadRandomAvatar());
[RelayCommand]
private void RemoveItem()
{
if (DataList.Count > 0)
{
DataList.RemoveAt(0);
}
}
private static Bitmap LoadRandomAvatar()
{
var uri = new Uri($"avares://HandyControlDemo/Resources/Img/Avatar/avatar{Random.Next(1, 7)}.png");
using var stream = AssetLoader.Open(uri);
return new Bitmap(stream);
}
}
@@ -20,7 +20,7 @@ public class ViewModelLocator
services.AddSingleton<DataService>();
services.AddTransient<MainViewModel>();
services.AddTransient<InputElementDemoViewModel>();
services.AddTransient<HoneycombPanelDemoViewModel>();
services.AddTransient<CardDemoViewModel>();
_serviceProvider = services.BuildServiceProvider();
}
@@ -29,5 +29,5 @@ public class ViewModelLocator
public InputElementDemoViewModel InputElementDemo => _serviceProvider.GetService<InputElementDemoViewModel>()!;
public HoneycombPanelDemoViewModel HoneycombPanelDemo => _serviceProvider.GetService<HoneycombPanelDemoViewModel>()!;
public CardDemoViewModel CardDemo => _serviceProvider.GetService<CardDemoViewModel>()!;
}
@@ -0,0 +1,62 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
namespace HandyControl.Controls;
public class Card : ContentControl
{
public static readonly StyledProperty<object?> HeaderProperty =
AvaloniaProperty.Register<Card, object?>(nameof(Header));
public object? Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
AvaloniaProperty.Register<Card, IDataTemplate?>(nameof(HeaderTemplate));
public IDataTemplate? HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
public static readonly StyledProperty<string?> HeaderStringFormatProperty =
AvaloniaProperty.Register<Card, string?>(nameof(HeaderStringFormat));
public string? HeaderStringFormat
{
get => GetValue(HeaderStringFormatProperty);
set => SetValue(HeaderStringFormatProperty, value);
}
public static readonly StyledProperty<object?> FooterProperty =
AvaloniaProperty.Register<Card, object?>(nameof(Footer));
public object? Footer
{
get => GetValue(FooterProperty);
set => SetValue(FooterProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> FooterTemplateProperty =
AvaloniaProperty.Register<Card, IDataTemplate?>(nameof(FooterTemplate));
public IDataTemplate? FooterTemplate
{
get => GetValue(FooterTemplateProperty);
set => SetValue(FooterTemplateProperty, value);
}
public static readonly StyledProperty<string?> FooterStringFormatProperty =
AvaloniaProperty.Register<Card, string?>(nameof(FooterStringFormat));
public string? FooterStringFormat
{
get => GetValue(FooterStringFormatProperty);
set => SetValue(FooterStringFormatProperty, value);
}
}
@@ -0,0 +1,51 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ControlTheme x:Key="CardBaseStyle"
TargetType="hc:Card">
<Setter Property="Background" Value="{DynamicResource RegionBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}" />
<Setter Property="Focusable" Value="False" />
<Setter Property="CornerRadius" Value="{StaticResource DefaultCornerRadius}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:Card">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border BorderThickness="0,0,0,1"
IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
</Border>
<ContentPresenter Grid.Row="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Border Grid.Row="2"
BorderThickness="0,1,0,0"
IsVisible="{TemplateBinding Footer, Converter={x:Static ObjectConverters.IsNotNull}}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter Content="{TemplateBinding Footer}"
ContentTemplate="{TemplateBinding FooterTemplate}" />
</Border>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:Card}"
BasedOn="{StaticResource CardBaseStyle}"
TargetType="hc:Card" />
</ResourceDictionary>
@@ -57,6 +57,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ContextMenu.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/SelectableTextBlock.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/HeaderedContentControl.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Card.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>