初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using FluentValidation;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public class MyTextBox : TextBox
|
||||
{
|
||||
public delegate void ValidateChangedEventHandler(object sender, EventArgs e);
|
||||
|
||||
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius",
|
||||
typeof(CornerRadius), typeof(MyTextBox), new PropertyMetadata(new CornerRadius(3d)));
|
||||
|
||||
public static readonly DependencyProperty ValidateResultProperty = DependencyProperty.Register("ValidateResult",
|
||||
typeof(string), typeof(MyTextBox),
|
||||
new PropertyMetadata("",
|
||||
(d, e) => d.SetValue(IsValidatedPropertyKey,
|
||||
string.IsNullOrEmpty((string)e.NewValue))));
|
||||
|
||||
private static readonly DependencyPropertyKey IsValidatedPropertyKey =
|
||||
DependencyProperty.RegisterReadOnly("IsValidated", typeof(bool), typeof(MyTextBox), new PropertyMetadata(true));
|
||||
|
||||
public static readonly DependencyProperty IsValidatedProperty = IsValidatedPropertyKey.DependencyProperty;
|
||||
|
||||
public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string),
|
||||
typeof(MyTextBox), new PropertyMetadata("", (t, e) =>
|
||||
{
|
||||
var textBox = (MyTextBox)t;
|
||||
textBox.UpdateHintText();
|
||||
}));
|
||||
|
||||
// 额外控件初始化
|
||||
|
||||
public List<RoutedEventHandler> changedEventList = new();
|
||||
|
||||
// 提示文本
|
||||
|
||||
/// <summary>
|
||||
/// 是否已经由用户输入过文本,若尚未输入过,则不显示输入检查的失败。
|
||||
/// </summary>
|
||||
private bool isTextChanged;
|
||||
|
||||
private ValidateState shownValidateResult = ValidateState.NotInited;
|
||||
|
||||
// 事件
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyTextBox()
|
||||
{
|
||||
Loaded += (_, _) => Validate();
|
||||
TextChanged += (a, b) => MyTextBox_TextChanged((MyTextBox)a, b);
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
GotFocus += (_, _) => RefreshColor();
|
||||
LostFocus += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshTextColor();
|
||||
}
|
||||
|
||||
// 自定义属性
|
||||
|
||||
public bool HasBackground { get; set; } = true;
|
||||
public bool ShowValidateResult { get; set; } = true;
|
||||
|
||||
public CornerRadius CornerRadius
|
||||
{
|
||||
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
private TextBlock labWrong
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Template is null) return null;
|
||||
if (field is null) field = (TextBlock)Template.FindName("labWrong", this);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
private TextBlock labHint
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Template is null) return null;
|
||||
if (field is null) field = (TextBlock)Template.FindName("labHint", this);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入验证
|
||||
|
||||
/// <summary>
|
||||
/// 输入验证结果。若为空字符串则无错误,否则为第一个错误原因。
|
||||
/// </summary>
|
||||
public string ValidateResult
|
||||
{
|
||||
get => (string)GetValue(ValidateResultProperty);
|
||||
set => SetValue(ValidateResultProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否通过了输入验证。
|
||||
/// </summary>
|
||||
public bool IsValidated => (bool)GetValue(IsValidatedProperty);
|
||||
|
||||
/// <summary>
|
||||
/// 输入验证的规则。
|
||||
/// </summary>
|
||||
public Collection<IValidator<string>> ValidateRules
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
Validate();
|
||||
}
|
||||
} = new();
|
||||
|
||||
public string HintText
|
||||
{
|
||||
get => (string)GetValue(HintTextProperty);
|
||||
set => SetValue(HintTextProperty, value);
|
||||
}
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
if (string.IsNullOrEmpty(HintText) || !string.IsNullOrEmpty(labHint.Text))
|
||||
return;
|
||||
UpdateHintText();
|
||||
}
|
||||
|
||||
public event ValidateChangedEventHandler? ValidateChanged;
|
||||
|
||||
public event RoutedEventHandler ValidatedTextChanged
|
||||
{
|
||||
add => changedEventList.Add(value);
|
||||
remove => changedEventList.Remove(value);
|
||||
}
|
||||
|
||||
private void OnValidatedTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
foreach (var handler in changedEventList)
|
||||
if (handler is not null)
|
||||
handler.Invoke(sender, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进行输入验证。
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
var stringResult = GetValidateResult();
|
||||
|
||||
ValidateResult = stringResult;
|
||||
// 根据结果改变样式
|
||||
if (shownValidateResult != (IsValidated ? ValidateState.Success : ValidateState.FailedAndShowDetail))
|
||||
{
|
||||
if (IsLoaded && labWrong is not null)
|
||||
ChangeValidateResult(IsValidated, true);
|
||||
else
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
Thread.Sleep(30);
|
||||
ModBase.RunInUi(() => ChangeValidateResult(IsValidated, false));
|
||||
}, "DelayedValidate Change");
|
||||
}
|
||||
|
||||
// 更新错误信息
|
||||
if (ShowValidateResult && !IsValidated)
|
||||
{
|
||||
if (IsLoaded && labWrong is not null)
|
||||
labWrong.Text = ValidateResult;
|
||||
else
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
var isFinished = false;
|
||||
while (!isFinished)
|
||||
{
|
||||
Thread.Sleep(20);
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (labWrong is not null)
|
||||
{
|
||||
labWrong.Text = ValidateResult;
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
if (!IsLoaded)
|
||||
isFinished = true;
|
||||
});
|
||||
}
|
||||
}, "DelayedValidate Text");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制显示结果为正常,类似尚未输入过文本的状态。不影响实际的检查结果。
|
||||
/// </summary>
|
||||
public void ForceShowAsSuccess()
|
||||
{
|
||||
isTextChanged = false;
|
||||
ChangeValidateResult(IsValidated, true);
|
||||
}
|
||||
|
||||
private void ChangeValidateResult(bool isSuccessful, bool isLoaded)
|
||||
{
|
||||
if (isLoaded && ModAnimation.AniControlEnabled == 0 && labWrong is not null)
|
||||
{
|
||||
if (isSuccessful || !isTextChanged)
|
||||
{
|
||||
// 变为正确
|
||||
shownValidateResult = isSuccessful ? ValidateState.Success : ValidateState.FailedButTextNotChanged;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(labWrong, -labWrong.Opacity, 150),
|
||||
ModAnimation.AaHeight(labWrong, -labWrong.Height, 150,
|
||||
ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() => labWrong.Visibility = Visibility.Collapsed, after: true)
|
||||
}, "MyTextBox Validate " + Uuid);
|
||||
}
|
||||
else if (ShowValidateResult)
|
||||
{
|
||||
// 变为错误
|
||||
shownValidateResult = ValidateState.FailedAndShowDetail;
|
||||
labWrong.Visibility = Visibility.Visible;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(labWrong, 1d - labWrong.Opacity, 150),
|
||||
ModAnimation.AaHeight(labWrong, 21d - labWrong.Height, 150,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyTextBox Validate " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 变为错误,但不显示文本
|
||||
shownValidateResult = ValidateState.FailedAndHideDetail;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
shownValidateResult = ValidateState.NotLoaded;
|
||||
}
|
||||
|
||||
RefreshColor();
|
||||
ValidateChanged?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
private void MyTextBox_TextChanged(MyTextBox sender, TextChangedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateHintText();
|
||||
// 改变输入记录
|
||||
isTextChanged = IsLoaded;
|
||||
// 进行输入验证
|
||||
Validate();
|
||||
if (!IsValidated)
|
||||
return;
|
||||
// 改变文本
|
||||
OnValidatedTextChanged(sender, e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"进行输入验证时出错",
|
||||
ModBase.LogLevel.Critical,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHintText()
|
||||
{
|
||||
if (labHint is null)
|
||||
return;
|
||||
labHint.Text = string.IsNullOrEmpty(Text) ? HintText : "";
|
||||
}
|
||||
|
||||
private string GetValidateResult()
|
||||
{
|
||||
var stringResult = string.Empty;
|
||||
// 执行输入验证
|
||||
foreach (var rule in ValidateRules)
|
||||
{
|
||||
var result = rule.Validate(Text);
|
||||
stringResult = result.IsValid ? "" : result.Errors[0].ErrorMessage;
|
||||
}
|
||||
|
||||
return stringResult;
|
||||
}
|
||||
|
||||
// 颜色
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 不对 ComboBox 从属进行动画
|
||||
if (TemplatedParent is not null && TemplatedParent is MyComboBox)
|
||||
return;
|
||||
// 判断当前颜色
|
||||
string foreColorName;
|
||||
string backColorName;
|
||||
int animationTime;
|
||||
if (IsEnabled)
|
||||
{
|
||||
if (IsValidated || !isTextChanged)
|
||||
{
|
||||
if (IsFocused)
|
||||
{
|
||||
foreColorName = "ColorBrush3";
|
||||
backColorName = "ColorBrush7";
|
||||
animationTime = 10;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
foreColorName = "ColorBrush4";
|
||||
backColorName = "ColorBrush7";
|
||||
animationTime = 100;
|
||||
}
|
||||
else // 未选中
|
||||
{
|
||||
foreColorName = "ColorBrushBg0";
|
||||
backColorName = "ColorBrushHalfWhite";
|
||||
animationTime = 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreColorName = "ColorBrushRedLight";
|
||||
backColorName = "ColorBrushRedBack";
|
||||
animationTime = 200;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreColorName = "ColorBrushGray5";
|
||||
backColorName = "ColorBrushGray6";
|
||||
animationTime = 200;
|
||||
}
|
||||
|
||||
if (!HasBackground)
|
||||
backColorName = "ColorBrushTransparent";
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, BorderBrushProperty, foreColorName, animationTime),
|
||||
ModAnimation.AaColor(this, BackgroundProperty, backColorName, animationTime)
|
||||
}, "MyTextBox Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyTextBox Color " + Uuid);
|
||||
SetResourceReference(BorderBrushProperty, foreColorName);
|
||||
SetResourceReference(BackgroundProperty, backColorName);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "文本框颜色改变出错");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTextColor()
|
||||
{
|
||||
var newColor = IsEnabled ? ThemeManager.colorGray1 : ThemeManager.colorGray4;
|
||||
if (((SolidColorBrush)Foreground).Color.R == newColor.r)
|
||||
return;
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0 && !string.IsNullOrEmpty(Text))
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, ForegroundProperty, IsEnabled ? "ColorBrushGray1" : "ColorBrushGray4",
|
||||
200)
|
||||
}, "MyTextBox TextColor " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyTextBox TextColor " + Uuid);
|
||||
Foreground = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 在按下回车时触发自定义事件
|
||||
private void MyTextBox_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter) ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
private enum ValidateState
|
||||
{
|
||||
NotInited,
|
||||
Success,
|
||||
FailedButTextNotChanged,
|
||||
FailedAndShowDetail,
|
||||
FailedAndHideDetail,
|
||||
NotLoaded
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user