初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,1112 @@
|
||||
using System.Collections;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using PCL.Core.UI.Controls.SvgIcon;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyListItem : IMyRadio
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
|
||||
|
||||
public delegate void LogoClickEventHandler(object sender, MouseButtonEventArgs e);
|
||||
|
||||
public bool isMouseOverAnimationEnabled = true;
|
||||
|
||||
private string stateLast;
|
||||
|
||||
public object tag { get; set; }
|
||||
public event IMyRadio.CheckEventHandler? Check;
|
||||
public event IMyRadio.ChangedEventHandler? Changed;
|
||||
|
||||
public event ClickEventHandler? Click;
|
||||
public event LogoClickEventHandler? LogoClick;
|
||||
|
||||
public void RefreshColor(object sender, EventArgs e)
|
||||
{
|
||||
// 菜单虚拟化检测
|
||||
if (ContentHandler is not null)
|
||||
{
|
||||
ContentHandler.Invoke(this, e);
|
||||
ContentHandler = null;
|
||||
}
|
||||
|
||||
// 判断当前颜色
|
||||
string stateNew;
|
||||
int time;
|
||||
if (isMouseDown && !(Type == CheckType.RadioBox && Checked))
|
||||
{
|
||||
stateNew = "MouseDown";
|
||||
time = 120;
|
||||
}
|
||||
else if (IsMouseOver && isMouseOverAnimationEnabled)
|
||||
{
|
||||
stateNew = "MouseOver";
|
||||
time = 120;
|
||||
}
|
||||
else
|
||||
{
|
||||
stateNew = "Idle";
|
||||
time = 180;
|
||||
}
|
||||
|
||||
if ((stateLast ?? "") == (stateNew ?? ""))
|
||||
return;
|
||||
stateLast = stateNew;
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
var ani = new List<ModAnimation.AniData>();
|
||||
if (IsMouseOver && isMouseOverAnimationEnabled)
|
||||
{
|
||||
if (buttonStack is not null)
|
||||
{
|
||||
ani.Add(ModAnimation.AaOpacity(buttonStack, 1d - buttonStack.Opacity, (int)Math.Round(time * 0.7d),
|
||||
(int)Math.Round(time * 0.3d)));
|
||||
ani.Add(ModAnimation.AaDouble(
|
||||
i => ColumnPaddingRight.Width =
|
||||
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
|
||||
Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25) - ColumnPaddingRight.Width.Value,
|
||||
(int)Math.Round(time * 0.3d), (int)Math.Round(time * 0.7d)));
|
||||
}
|
||||
|
||||
ani.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
|
||||
isMouseDown ? "ColorBrush6" : "ColorBrushBg1", time),
|
||||
ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, time,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
});
|
||||
if (IsScaleAnimationEnabled)
|
||||
{
|
||||
ani.Add(ModAnimation.AaScaleTransform(RectBack,
|
||||
1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(time * 1.6d),
|
||||
ease: new ModAnimation.AniEaseOutFluent()));
|
||||
if (isMouseDown)
|
||||
ani.Add(ModAnimation.AaScaleTransform(this, 0.98d - ((ScaleTransform)RenderTransform).ScaleX,
|
||||
(int)Math.Round(time * 0.9d), ease: new ModAnimation.AniEaseOutFluent()));
|
||||
else
|
||||
ani.Add(ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX,
|
||||
(int)Math.Round(time * 1.2d), ease: new ModAnimation.AniEaseOutFluent()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buttonStack is not null)
|
||||
{
|
||||
ani.Add(ModAnimation.AaOpacity(buttonStack, -buttonStack.Opacity, (int)Math.Round(time * 0.4d)));
|
||||
ani.Add(ModAnimation.AaDouble(
|
||||
i => ColumnPaddingRight.Width =
|
||||
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
|
||||
MinPaddingRight - ColumnPaddingRight.Width.Value, (int)Math.Round(time * 0.4d)));
|
||||
}
|
||||
|
||||
ani.Add(ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, time));
|
||||
if (IsScaleAnimationEnabled)
|
||||
ani.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
|
||||
isMouseDown ? "ColorBrush6" : "ColorBrush7", time),
|
||||
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, time * 3,
|
||||
ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaScaleTransform(RectBack,
|
||||
0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, time,
|
||||
ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaScaleTransform(RectBack, -0.246d, 1, after: true)
|
||||
});
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(ani, "ListItem Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
if (IsMouseOver && isMouseOverAnimationEnabled)
|
||||
{
|
||||
if (buttonStack is not null)
|
||||
{
|
||||
buttonStack.Opacity = 1d;
|
||||
ColumnPaddingRight.Width = new GridLength(Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25));
|
||||
}
|
||||
|
||||
// 由于鼠标已经移入,所以直接实例化 RectBack
|
||||
RectBack.Background = (Brush)ThemeManager.AppResources["ColorBrushBg1"];
|
||||
RectBack.Opacity = 1d;
|
||||
RectBack.RenderTransform = new ScaleTransform(1d, 1d);
|
||||
RenderTransform = new ScaleTransform(1d, 1d);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buttonStack is not null)
|
||||
{
|
||||
buttonStack.Opacity = 0d;
|
||||
ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
|
||||
}
|
||||
|
||||
RenderTransform = new ScaleTransform(1d, 1d);
|
||||
if (_RectBack is not null)
|
||||
{
|
||||
if (IsScaleAnimationEnabled)
|
||||
RectBack.RenderTransform = new ScaleTransform(0.75d, 0.75d);
|
||||
RectBack.Background = (Brush)ThemeManager.AppResources["ColorBrush7"];
|
||||
RectBack.Opacity = 0d;
|
||||
}
|
||||
}
|
||||
|
||||
ModAnimation.AniStop("ListItem Color " + Uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private void MyListItem_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Checked)
|
||||
SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
|
||||
else
|
||||
SetResourceReference(ForegroundProperty, "ColorBrush1");
|
||||
ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Title;
|
||||
}
|
||||
|
||||
#region 后加载控件
|
||||
|
||||
// 指向背景
|
||||
private Border _RectBack;
|
||||
|
||||
public Border RectBack
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_RectBack is null)
|
||||
{
|
||||
var rect = new Border
|
||||
{
|
||||
Name = "RectBack",
|
||||
CornerRadius = new CornerRadius(IsScaleAnimationEnabled || Height > 40d ? 6 : 0),
|
||||
RenderTransform = IsScaleAnimationEnabled ? new ScaleTransform(0.8d, 0.8d) : null,
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d),
|
||||
BorderThickness = new Thickness(ModBase.GetWPFSize(1d)),
|
||||
SnapsToDevicePixels = true,
|
||||
IsHitTestVisible = false,
|
||||
Opacity = 0d
|
||||
};
|
||||
rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7");
|
||||
rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6");
|
||||
SetColumnSpan(rect, 999);
|
||||
SetRowSpan(rect, 999);
|
||||
Children.Insert(0, rect);
|
||||
_RectBack = rect;
|
||||
// <!--<Border x:Name = "RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True"
|
||||
// IsHitTestVisible = "False" Opacity="0" BorderThickness="1"
|
||||
// Grid.ColumnSpan = "4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}"/>-->
|
||||
}
|
||||
|
||||
return _RectBack;
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮
|
||||
public FrameworkElement buttonStack;
|
||||
|
||||
// 图标
|
||||
public FrameworkElement pathLogo;
|
||||
|
||||
// 勾选条
|
||||
public Border rectCheck;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tags 的存放 StackPanel
|
||||
/// </summary>
|
||||
public StackPanel PanTags
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is not null)
|
||||
return field;
|
||||
var newStack = new StackPanel
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Orientation = Orientation.Horizontal,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(3.5d, 0d, -3, 0d)
|
||||
};
|
||||
SetColumn(newStack, 3);
|
||||
SetRow(newStack, 2);
|
||||
PanBack.Children.Add(newStack);
|
||||
field = newStack;
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标签,可以传入 String 和 List(Of String)
|
||||
/// </summary>
|
||||
public object Tags
|
||||
{
|
||||
set
|
||||
{
|
||||
var list = new List<string>();
|
||||
if (value is string str) list = str.Split("|").ToList();
|
||||
if (value is List<string>) list = (List<string>)value;
|
||||
PanTags.Children.Clear();
|
||||
PanTags.Visibility = list.Any() ? Visibility.Visible : Visibility.Collapsed;
|
||||
foreach (var TagText in list)
|
||||
{
|
||||
var newTag = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(17, 0, 0, 0)),
|
||||
Padding = new Thickness(3d, 1d, 3d, 1d),
|
||||
CornerRadius = new CornerRadius(3d),
|
||||
Margin = new Thickness(0d, 0d, 3d, 0d),
|
||||
SnapsToDevicePixels = true,
|
||||
UseLayoutRounding = false
|
||||
};
|
||||
var tagTextBlock = new TextBlock
|
||||
{
|
||||
Text = TagText,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(134, 134, 134)),
|
||||
FontSize = 11d
|
||||
};
|
||||
newTag.Child = tagTextBlock;
|
||||
PanTags.Children.Add(newTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 副文本
|
||||
|
||||
public TextBlock LabInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
{
|
||||
var lab = new TextBlock
|
||||
{
|
||||
Name = "LabInfo",
|
||||
SnapsToDevicePixels = false,
|
||||
UseLayoutRounding = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
IsHitTestVisible = false,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Visibility = Visibility.Collapsed,
|
||||
FontSize = 12d,
|
||||
Margin = new Thickness(4d, 0d, 0d, 0d),
|
||||
Opacity = 0.6d
|
||||
};
|
||||
SetColumn(lab, 4);
|
||||
SetRow(lab, 2);
|
||||
PanBack.Children.Add(lab);
|
||||
field = lab;
|
||||
// <TextBlock Grid.Row="2" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left" x:Name = "LabInfo" IsHitTestVisible="False" Grid.Column="2"
|
||||
// TextTrimming = "CharacterEllipsis" Visibility="Collapsed" FontSize="12" Foreground="{StaticResource ColorBrushGray2}" Margin="4,0,0,0" />
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 自定义属性
|
||||
|
||||
// Uuid
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用缩放动画。
|
||||
/// </summary>
|
||||
public bool IsScaleAnimationEnabled
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
if (_RectBack is not null)
|
||||
RectBack.CornerRadius = new CornerRadius(value ? 6 : 0);
|
||||
}
|
||||
} = true;
|
||||
|
||||
// 边距
|
||||
public int PaddingLeft
|
||||
{
|
||||
get => (int)Math.Round(ColumnPaddingLeft.Width.Value);
|
||||
set => ColumnPaddingLeft.Width = new GridLength(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 右边距的最小值。
|
||||
/// 在存在右侧按钮时,右边距会被自动设置为 5 + 按钮数 * 25。
|
||||
/// </summary>
|
||||
public int MinPaddingRight { get; set; } = 4;
|
||||
|
||||
// 按钮
|
||||
|
||||
public IEnumerable<MyIconButton> Buttons
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
// 没有特殊按钮,移除原 Stack
|
||||
if (buttonStack is not null)
|
||||
{
|
||||
Children.Remove(buttonStack);
|
||||
buttonStack = null;
|
||||
}
|
||||
|
||||
// 添加新 Stack
|
||||
switch (value.Count())
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
break;
|
||||
}
|
||||
// 没有按钮,不添加新的
|
||||
case 1:
|
||||
{
|
||||
// 只有一个按钮
|
||||
foreach (var Btn in value)
|
||||
{
|
||||
if (Btn.Height.Equals(double.NaN))
|
||||
Btn.Height = 25d;
|
||||
if (Btn.Width.Equals(double.NaN))
|
||||
Btn.Width = 25d;
|
||||
Btn.Opacity = 0d;
|
||||
Btn.Margin = new Thickness(0d, 0d, 5d, 0d);
|
||||
Btn.SnapsToDevicePixels = false;
|
||||
Btn.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
Btn.VerticalAlignment = VerticalAlignment.Center;
|
||||
Btn.UseLayoutRounding = false;
|
||||
SetColumnSpan(Btn, 10);
|
||||
SetRowSpan(Btn, 10);
|
||||
Children.Add(Btn);
|
||||
buttonStack = Btn;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// 有复数按钮,使用 StackPanel
|
||||
buttonStack = new StackPanel
|
||||
{
|
||||
Opacity = 0d, Margin = new Thickness(0d, 0d, 5d, 0d), SnapsToDevicePixels = false,
|
||||
Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center, UseLayoutRounding = false
|
||||
};
|
||||
SetColumnSpan(buttonStack, 10);
|
||||
SetRowSpan(buttonStack, 10);
|
||||
// 构造按钮
|
||||
foreach (var Btn in value)
|
||||
{
|
||||
if (Btn.Height.Equals(double.NaN))
|
||||
Btn.Height = 25d;
|
||||
if (Btn.Width.Equals(double.NaN))
|
||||
Btn.Width = 25d;
|
||||
((StackPanel)buttonStack).Children.Add(Btn);
|
||||
}
|
||||
|
||||
Children.Add(buttonStack);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标题
|
||||
public InlineCollection Inlines => LabTitle.Inlines;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value.Replace("\r", "").Replace("\n", ""));
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register("Title", typeof(string), typeof(MyListItem));
|
||||
|
||||
// 字号
|
||||
public double FontSize
|
||||
{
|
||||
get => (double)GetValue(FontSizeProperty);
|
||||
set => SetValue(FontSizeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty FontSizeProperty =
|
||||
DependencyProperty.Register("FontSize", typeof(double), typeof(MyListItem), new PropertyMetadata(14d));
|
||||
|
||||
// 信息
|
||||
public string Info
|
||||
{
|
||||
get => (string)GetValue(InfoProperty);
|
||||
set
|
||||
{
|
||||
if (Info == value)
|
||||
return;
|
||||
value = value?.Replace("\r", "").Replace("\n", "");
|
||||
SetValue(InfoProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty InfoProperty = DependencyProperty.Register("Info", typeof(string),
|
||||
typeof(MyListItem), new PropertyMetadata("", OnInfoChanged));
|
||||
|
||||
public MyListItem()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
SizeChanged += (_, _) => OnSizeChanged();
|
||||
PreviewMouseLeftButtonUp += Button_MouseUp;
|
||||
PreviewMouseLeftButtonDown += Button_MouseDown;
|
||||
MouseLeave += Button_MouseLeave;
|
||||
PreviewMouseLeftButtonUp += Button_MouseLeave;
|
||||
MouseEnter += RefreshColor;
|
||||
MouseLeave += RefreshColor;
|
||||
MouseLeftButtonDown += RefreshColor;
|
||||
MouseLeftButtonUp += RefreshColor;
|
||||
Loaded += MyListItem_Loaded;
|
||||
}
|
||||
|
||||
private static void OnInfoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (MyListItem)d;
|
||||
var value = e.NewValue as string;
|
||||
control.LabInfo.Text = value;
|
||||
control.LabInfo.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
// 图片
|
||||
public string Logo
|
||||
{
|
||||
get => (string)GetValue(LogoProperty);
|
||||
set
|
||||
{
|
||||
if (Logo == value)
|
||||
return;
|
||||
SetValue(LogoProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LogoProperty = DependencyProperty.Register(
|
||||
nameof(Logo),
|
||||
typeof(string),
|
||||
typeof(MyListItem),
|
||||
new PropertyMetadata("", OnLogoChanged));
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get => (string)GetValue(SvgIconProperty);
|
||||
set
|
||||
{
|
||||
if (SvgIcon == value)
|
||||
return;
|
||||
SetValue(SvgIconProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SvgIconProperty = DependencyProperty.Register(
|
||||
nameof(SvgIcon),
|
||||
typeof(string),
|
||||
typeof(MyListItem),
|
||||
new PropertyMetadata("", OnSvgIconChanged));
|
||||
|
||||
private static void OnLogoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (MyListItem)d;
|
||||
control.UpdateLogo(e.NewValue as string);
|
||||
}
|
||||
|
||||
private static void OnSvgIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (MyListItem)d;
|
||||
control.UpdateLogo(control.Logo);
|
||||
}
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private void UpdateLogo(string logo)
|
||||
{
|
||||
// 删除旧 Logo
|
||||
if (pathLogo is not null)
|
||||
Children.Remove(pathLogo);
|
||||
pathLogo = null;
|
||||
|
||||
// 添加新 Logo
|
||||
if (IsUsingSvgIcon)
|
||||
{
|
||||
pathLogo = new SvgIcon
|
||||
{
|
||||
Tag = this,
|
||||
IsHitTestVisible = LogoClickable,
|
||||
Icon = SvgIcon,
|
||||
Stretch = Stretch.Uniform,
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d),
|
||||
RenderTransform = new ScaleTransform { ScaleX = 1D, ScaleY = 1D },
|
||||
SnapsToDevicePixels = false,
|
||||
UseLayoutRounding = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch
|
||||
};
|
||||
((SvgIcon)pathLogo).SetBinding(
|
||||
Core.UI.Controls.SvgIcon.SvgIcon.IconBrushProperty,
|
||||
new Binding("Foreground") { Source = this });
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(logo))
|
||||
{
|
||||
if (logo.StartsWithF("http", true))
|
||||
{
|
||||
// 网络图片
|
||||
pathLogo = new MyImage
|
||||
{
|
||||
Tag = this,
|
||||
IsHitTestVisible = LogoClickable,
|
||||
Source = logo,
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d),
|
||||
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
|
||||
SnapsToDevicePixels = true,
|
||||
UseLayoutRounding = false
|
||||
};
|
||||
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.Linear);
|
||||
}
|
||||
else if (logo.EndsWithF(".png", true) || logo.EndsWithF(".jpg", true) || logo.EndsWithF(".webp", true))
|
||||
{
|
||||
// 位图
|
||||
pathLogo = new Canvas
|
||||
{
|
||||
Tag = this,
|
||||
IsHitTestVisible = LogoClickable,
|
||||
Background = new MyBitmap(logo),
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d),
|
||||
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
|
||||
SnapsToDevicePixels = true,
|
||||
UseLayoutRounding = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch
|
||||
};
|
||||
if (logo.Contains(ModBase.pathTemp + @"Cache\Skin\Head") ||
|
||||
logo.Contains(ModBase.pathTemp + @"Cache\Cape"))
|
||||
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.NearestNeighbor);
|
||||
else
|
||||
RenderOptions.SetBitmapScalingMode(pathLogo, BitmapScalingMode.Linear);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 矢量图
|
||||
pathLogo = new Path
|
||||
{
|
||||
Tag = this,
|
||||
IsHitTestVisible = LogoClickable,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Stretch = Stretch.Uniform,
|
||||
Data = (Geometry)new GeometryConverter().ConvertFromString(logo),
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d),
|
||||
RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
|
||||
SnapsToDevicePixels = false,
|
||||
UseLayoutRounding = false
|
||||
};
|
||||
pathLogo.SetBinding(Shape.FillProperty, new Binding("Foreground") { Source = this });
|
||||
}
|
||||
}
|
||||
|
||||
if (pathLogo is not null)
|
||||
{
|
||||
SetColumn(pathLogo, 2);
|
||||
SetRowSpan(pathLogo, 4);
|
||||
OnSizeChanged(); // 设置边距
|
||||
Children.Add(pathLogo);
|
||||
// 图标的点击事件
|
||||
if (LogoClickable)
|
||||
{
|
||||
pathLogo.MouseLeave += (sender, e) => isLogoDown = false;
|
||||
pathLogo.MouseLeftButtonDown += (sender, e) => isLogoDown = true;
|
||||
pathLogo.MouseLeftButtonUp += (sender, e) =>
|
||||
{
|
||||
if (isLogoDown)
|
||||
{
|
||||
isLogoDown = false;
|
||||
LogoClick?.Invoke(((FrameworkElement)sender).Tag, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 改变行距
|
||||
var hasLogo = IsUsingSvgIcon || !string.IsNullOrEmpty(logo);
|
||||
ColumnLogo.Width = new GridLength((hasLogo ? 34 : 0) + (Height < 40d ? 0 : 4));
|
||||
}
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
if (pathLogo is not null)
|
||||
{
|
||||
var scale = IsUsingSvgIcon ? 1D : LogoScale;
|
||||
pathLogo.RenderTransform = new ScaleTransform { ScaleX = scale, ScaleY = scale };
|
||||
}
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
// 图标的点击
|
||||
/// <summary>
|
||||
/// 该 Logo 是否可用点击触发事件。需要在 Logo 属性之前设置。
|
||||
/// </summary>
|
||||
public bool LogoClickable { get; set; } = false;
|
||||
|
||||
private bool isLogoDown;
|
||||
|
||||
// 勾选选项
|
||||
public enum CheckType
|
||||
{
|
||||
None,
|
||||
Clickable,
|
||||
RadioBox,
|
||||
CheckBox
|
||||
}
|
||||
|
||||
private CheckType _Type = CheckType.None;
|
||||
|
||||
public CheckType Type
|
||||
{
|
||||
get => _Type;
|
||||
set
|
||||
{
|
||||
if (_Type == value)
|
||||
return;
|
||||
_Type = value;
|
||||
// 切换左栏大小
|
||||
ColumnCheck.Width =
|
||||
new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6);
|
||||
// 切换竖条控件
|
||||
if (_Type == CheckType.None || _Type == CheckType.Clickable)
|
||||
{
|
||||
// 移除竖条控件
|
||||
if (rectCheck is not null)
|
||||
{
|
||||
Children.Remove(rectCheck);
|
||||
rectCheck = null;
|
||||
}
|
||||
|
||||
SetChecked(false, false, false);
|
||||
}
|
||||
// 添加竖条控件
|
||||
else if (rectCheck is null)
|
||||
{
|
||||
rectCheck = new Border
|
||||
{
|
||||
Width = 5d,
|
||||
Height = Checked ? double.NaN : 0d,
|
||||
CornerRadius = new CornerRadius(2d, 2d, 2d, 2d),
|
||||
VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
UseLayoutRounding = false,
|
||||
SnapsToDevicePixels = false,
|
||||
Margin = Checked ? new Thickness(-1, 6d, 0d, 6d) : new Thickness(-1, 0d, 0d, 0d)
|
||||
};
|
||||
rectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3");
|
||||
SetRowSpan(rectCheck, 4);
|
||||
Children.Add(rectCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适应尺寸
|
||||
private void OnSizeChanged()
|
||||
{
|
||||
var logo = Logo;
|
||||
var hasLogo = IsUsingSvgIcon || !string.IsNullOrEmpty(logo);
|
||||
ColumnCheck.Width = new GridLength(_Type is CheckType.None or CheckType.Clickable
|
||||
? Height < 40d
|
||||
? 4
|
||||
: 2
|
||||
: 6);
|
||||
ColumnLogo.Width = new GridLength((hasLogo ? 34 : 0) + (Height < 40d ? 0 : 4));
|
||||
|
||||
if (pathLogo is not null)
|
||||
{
|
||||
if (!IsUsingSvgIcon && (logo.EndsWithF(".png", true) ||
|
||||
logo.EndsWithF(".jpg", true) ||
|
||||
logo.EndsWithF(".webp", true)))
|
||||
pathLogo.Margin = new Thickness(4d, 5d, 3d, 5d);
|
||||
else
|
||||
pathLogo.Margin = new Thickness(Height < 40d ? 6 : 8, 8d, Height < 40d ? 4 : 6, 8d);
|
||||
}
|
||||
|
||||
LabTitle.Margin = new Thickness(4d, 0d, 0d, Height < 40d ? 0 : 2);
|
||||
}
|
||||
|
||||
// 勾选状态
|
||||
private bool _Checked;
|
||||
|
||||
public bool Checked
|
||||
{
|
||||
get => _Checked;
|
||||
set => SetChecked(value, false, value != _Checked); // 仅在值发生变化时触发动画 (#4596)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动设置 Checked 属性。
|
||||
/// </summary>
|
||||
/// <param name="value">新的 Checked 属性。</param>
|
||||
/// <param name="user">是否由用户引发。</param>
|
||||
/// <param name="anime">是否执行动画。</param>
|
||||
public void SetChecked(bool value, bool user, bool anime)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 自定义属性基础
|
||||
|
||||
var ChangedEventArgs = new ModBase.RouteEventArgs(user);
|
||||
var rawValue = _Checked;
|
||||
if (Type == CheckType.RadioBox)
|
||||
{
|
||||
if (IsInitialized && value != _Checked)
|
||||
{
|
||||
_Checked = value;
|
||||
Changed?.Invoke(this, ChangedEventArgs);
|
||||
if (ChangedEventArgs.handled)
|
||||
{
|
||||
_Checked = rawValue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_Checked = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value == _Checked)
|
||||
return;
|
||||
_Checked = value;
|
||||
if (IsInitialized)
|
||||
{
|
||||
Changed?.Invoke(this, ChangedEventArgs);
|
||||
if (ChangedEventArgs.handled)
|
||||
{
|
||||
_Checked = rawValue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
var checkEventArgs = new ModBase.RouteEventArgs(user);
|
||||
Check?.Invoke(this, checkEventArgs);
|
||||
if (checkEventArgs.handled)
|
||||
return;
|
||||
}
|
||||
|
||||
// 保证只有一个单选 ListItem 选中
|
||||
|
||||
if (Type == CheckType.RadioBox)
|
||||
{
|
||||
if (Parent is null)
|
||||
return;
|
||||
var radioboxList = new List<MyListItem>();
|
||||
var checkedCount = 0;
|
||||
// 收集控件列表与选中个数
|
||||
foreach (var ControlRaw in ((Panel)Parent).Children)
|
||||
{
|
||||
var control = MyVirtualizingElement.TryInit((FrameworkElement)ControlRaw);
|
||||
if (control is MyListItem listItem && listItem.Type == CheckType.RadioBox)
|
||||
{
|
||||
radioboxList.Add(listItem);
|
||||
if (listItem.Checked)
|
||||
checkedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断选中情况
|
||||
switch (checkedCount)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
// 没有任何单选框被选中,选择第一个
|
||||
radioboxList[0].Checked = true;
|
||||
break;
|
||||
}
|
||||
case var @case when @case > 1:
|
||||
{
|
||||
// 选中项目多于 1 个
|
||||
if (Checked)
|
||||
{
|
||||
// 如果本控件选中,则取消其他所有控件的选中
|
||||
foreach (var Control in radioboxList)
|
||||
if (Control.Checked && !Control.Equals(this))
|
||||
Control.Checked = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果本控件未选中,则只保留第一个选中的控件
|
||||
var firstChecked = false;
|
||||
foreach (var Control in radioboxList)
|
||||
if (Control.Checked)
|
||||
{
|
||||
if (firstChecked)
|
||||
Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发
|
||||
else
|
||||
firstChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var customHeight = 20d; // 修改左侧神秘动画条的高度
|
||||
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0 && anime) // 防止默认属性变更触发动画
|
||||
{
|
||||
var anim = new List<ModAnimation.AniData>();
|
||||
if (Checked)
|
||||
{
|
||||
// 由无变有
|
||||
if (rectCheck is not null)
|
||||
{
|
||||
// 统一静态属性:固定高度、居中对齐、无额外边距
|
||||
rectCheck.Height = customHeight;
|
||||
rectCheck.VerticalAlignment = VerticalAlignment.Center;
|
||||
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
|
||||
rectCheck.Opacity = 1d;
|
||||
|
||||
// 初始化缩放中心为正中心
|
||||
var scale = new ScaleTransform(1d, 0d);
|
||||
rectCheck.RenderTransformOrigin = new Point(0.5d, 0.5d);
|
||||
rectCheck.RenderTransform = scale;
|
||||
|
||||
// 动画:让 ScaleY 从 0 弹性放大到 1
|
||||
anim.Add(ModAnimation.AaDouble(
|
||||
i => scale.ScaleY = Math.Max(0d, scale.ScaleY + (double)i),
|
||||
1d - scale.ScaleY,
|
||||
300,
|
||||
ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)
|
||||
));
|
||||
}
|
||||
|
||||
anim.Add(ModAnimation.AaColor(this, ForegroundProperty,
|
||||
Height < 40d ? "ColorBrush3" : "ColorBrush2", 200));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 由有变无
|
||||
if (rectCheck is not null)
|
||||
{
|
||||
if (rectCheck.RenderTransform is not ScaleTransform)
|
||||
{
|
||||
rectCheck.RenderTransformOrigin = new Point(0.5d, 0.5d);
|
||||
rectCheck.RenderTransform = new ScaleTransform(1d, 1d);
|
||||
}
|
||||
|
||||
var scale = (ScaleTransform)rectCheck.RenderTransform;
|
||||
|
||||
// 动画:让 ScaleY 从当前值缩减到 0
|
||||
anim.Add(ModAnimation.AaDouble(
|
||||
i => scale.ScaleY = Math.Max(0d, scale.ScaleY + (double)i),
|
||||
-scale.ScaleY,
|
||||
120,
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)
|
||||
));
|
||||
anim.Add(ModAnimation.AaOpacity(rectCheck, -rectCheck.Opacity, 70, 40));
|
||||
}
|
||||
|
||||
anim.Add(ModAnimation.AaColor(this, ForegroundProperty, "ColorBrush1", 120));
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(anim, "MyListItem Checked " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不使用动画
|
||||
ModAnimation.AniStop("MyListItem Checked " + Uuid);
|
||||
if (Checked)
|
||||
{
|
||||
if (rectCheck is not null)
|
||||
{
|
||||
rectCheck.Height = customHeight; // 应用自定义固定高度
|
||||
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
|
||||
rectCheck.Opacity = 1d;
|
||||
rectCheck.VerticalAlignment = VerticalAlignment.Center; // 居中
|
||||
rectCheck.RenderTransform = null; // 清除缩放
|
||||
}
|
||||
|
||||
SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rectCheck is not null)
|
||||
{
|
||||
rectCheck.Height = 0d;
|
||||
rectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
|
||||
rectCheck.Opacity = 0d;
|
||||
rectCheck.VerticalAlignment = VerticalAlignment.Center;
|
||||
rectCheck.RenderTransform = null;
|
||||
}
|
||||
|
||||
SetResourceReference(ForegroundProperty, "ColorBrush1");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "设置 Checked 失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 前景色绑定
|
||||
public Brush Foreground
|
||||
{
|
||||
get => (Brush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register("Foreground",
|
||||
typeof(Brush), typeof(MyListItem), new PropertyMetadata(ThemeManager.AppResources["ColorBrush1"]));
|
||||
|
||||
// 菜单与按钮绑定
|
||||
public Action<MyListItem, EventArgs> ContentHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标可选的圆角。仅当 Logo 为位图(MyImage 或 Canvas)时生效。
|
||||
/// 当所有角的圆角半径均 ≥ 0 时,应用圆角效果;否则不应用。
|
||||
/// </summary>
|
||||
public CornerRadius LogoCornerRadius
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
if (pathLogo is Canvas) _canvasClipHandlerAdded = false;
|
||||
ApplyLogoCornerRadius();
|
||||
}
|
||||
} = new CornerRadius(-1);
|
||||
|
||||
private bool IsLogoCornerRadiusEnabled() =>
|
||||
LogoCornerRadius is { TopLeft: >= 0, TopRight: >= 0, BottomLeft: >= 0, BottomRight: >= 0 };
|
||||
|
||||
private void UpdateCanvasClip(Canvas c)
|
||||
{
|
||||
if (c is { ActualWidth: > 0, ActualHeight: > 0 })
|
||||
{
|
||||
var r = LogoCornerRadius;
|
||||
double radius = Math.Max(Math.Max(r.TopLeft, r.TopRight), Math.Max(r.BottomLeft, r.BottomRight));
|
||||
c.Clip = new RectangleGeometry(new Rect(0, 0, c.ActualWidth, c.ActualHeight), radius, radius);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _canvasClipHandlerAdded = false;
|
||||
|
||||
private void ApplyLogoCornerRadius()
|
||||
{
|
||||
if (pathLogo is null || !IsLogoCornerRadiusEnabled()) return;
|
||||
|
||||
switch (pathLogo)
|
||||
{
|
||||
case MyImage myImage:
|
||||
myImage.CornerRadius = LogoCornerRadius;
|
||||
break;
|
||||
case Canvas canvas when !_canvasClipHandlerAdded && canvas is { ActualWidth: 0, ActualHeight: 0 }:
|
||||
UpdateCanvasClip(canvas);
|
||||
canvas.SizeChanged += OnCanvasLogoSizeChanged;
|
||||
_canvasClipHandlerAdded = true;
|
||||
break;
|
||||
case Canvas canvas:
|
||||
UpdateCanvasClip(canvas);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanvasLogoSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (sender is not Canvas { ActualWidth: > 0, ActualHeight: > 0 } canvas) return;
|
||||
|
||||
UpdateCanvasClip(canvas);
|
||||
canvas.SizeChanged -= OnCanvasLogoSizeChanged;
|
||||
_canvasClipHandlerAdded = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 点击
|
||||
|
||||
// 触发点击事件
|
||||
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
Click?.Invoke(sender, e);
|
||||
if (e.Handled)
|
||||
return;
|
||||
// 触发自定义事件
|
||||
var dependencyObject = (DependencyObject)sender;
|
||||
if (CustomEventService.GetEventType(dependencyObject) != EventType.None)
|
||||
{
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
if (e.Handled)
|
||||
return;
|
||||
// 实际的单击处理
|
||||
switch (Type)
|
||||
{
|
||||
case CheckType.Clickable:
|
||||
{
|
||||
ModBase.Log("[Control] 按下单击列表项:" + Title);
|
||||
break;
|
||||
}
|
||||
case CheckType.RadioBox:
|
||||
{
|
||||
ModBase.Log("[Control] 按下单选列表项:" + Title);
|
||||
if (!Checked)
|
||||
SetChecked(true, true, true);
|
||||
break;
|
||||
}
|
||||
case CheckType.CheckBox:
|
||||
{
|
||||
ModBase.Log("[Control] 按下复选列表项(" + !Checked + "):" + Title);
|
||||
SetChecked(!Checked, true, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标点击判定
|
||||
private bool isMouseDown;
|
||||
|
||||
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (IsMouseDirectlyOver && !(Type == CheckType.None))
|
||||
{
|
||||
isMouseDown = true;
|
||||
if (buttonStack is not null)
|
||||
buttonStack.IsHitTestVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_MouseLeave(object sender, object e)
|
||||
{
|
||||
isMouseDown = false;
|
||||
if (buttonStack is not null)
|
||||
buttonStack.IsHitTestVisible = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user