初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
@@ -0,0 +1,670 @@
|
||||
<Application x:Class="PCL.Application"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:behaviors="clr-namespace:PCL.Controls.Behaviors"
|
||||
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
|
||||
xmlns:cvt="clr-namespace:PCL.Core.UI.Converters;assembly=PCL.Core"
|
||||
xmlns:localization="clr-namespace:PCL.Core.App.Localization;assembly=PCL.Core"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Resources/Markdown.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<sys:Double x:Key="BlurRadius">0.0</sys:Double>
|
||||
<sys:Double x:Key="BlurSamplingRate">0.7</sys:Double>
|
||||
<KernelType x:Key="BlurType">Gaussian</KernelType>
|
||||
|
||||
<!-- Converter -->
|
||||
<local:AdditionConverter x:Key="AdditionConverter" />
|
||||
<local:MultiplicationConverter x:Key="MultiplicationConverter" />
|
||||
<local:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
|
||||
<local:InverseBooleanConverter x:Key="InverseBooleanConverter" />
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
<cvt:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<localization:LocalizationFormatConverter x:Key="LocalizationFormatConverter" />
|
||||
|
||||
<!--字体-->
|
||||
<FontFamily x:Key="LaunchFontFamily">Resources/#PCL English, Microsoft YaHei UI, Segoe UI</FontFamily>
|
||||
|
||||
<!-- 颜色表 -->
|
||||
<SolidColorBrush x:Key="ColorBrush1">#343d4a</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush2">#0b5bcb</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush3">#1370f3</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush4">#4890f5</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush5">#96c0f9</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush6">#d5e6fd</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush7">#e0eafd</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrush8">#eaf2fe</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushBg0">#96c0f9</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushBg1">#bee0eafd</SolidColorBrush>
|
||||
<Color x:Key="ColorObject1">#343d4a</Color>
|
||||
<Color x:Key="ColorObject2">#0b5bcb</Color>
|
||||
<Color x:Key="ColorObject3">#1370f3</Color>
|
||||
<Color x:Key="ColorObject4">#4890f5</Color>
|
||||
<Color x:Key="ColorObject5">#96c0f9</Color>
|
||||
<Color x:Key="ColorObject6">#d5e6fd</Color>
|
||||
<Color x:Key="ColorObject7">#e0eafd</Color>
|
||||
<Color x:Key="ColorObject8">#eaf2fe</Color>
|
||||
<Color x:Key="ColorObjectBg0">#96c0f9</Color>
|
||||
<Color x:Key="ColorObjectBg1">#bee0eafd</Color>
|
||||
<Color x:Key="ColorObjectMsgBoxShadow">#3c3c3c</Color>
|
||||
<SolidColorBrush x:Key="ColorBrushGray1">#404040</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray2">#737373</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray3">#8c8c8c</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray4">#a6a6a6</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray5">#cccccc</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray6">#ebebeb</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray7">#f0f0f0</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushGray8">#f5f5f5</SolidColorBrush>
|
||||
<Color x:Key="ColorObjectGray1">#404040</Color>
|
||||
<Color x:Key="ColorObjectGray2">#737373</Color>
|
||||
<Color x:Key="ColorObjectGray3">#8c8c8c</Color>
|
||||
<Color x:Key="ColorObjectGray4">#a6a6a6</Color>
|
||||
<Color x:Key="ColorObjectGray5">#cccccc</Color>
|
||||
<Color x:Key="ColorObjectGray6">#ebebeb</Color>
|
||||
<Color x:Key="ColorObjectGray7">#f0f0f0</Color>
|
||||
<Color x:Key="ColorObjectGray8">#f5f5f5</Color>
|
||||
<SolidColorBrush x:Key="ColorBrushRedBack">#80fbdddd</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushRedLight">#ff4c4c</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushRedDark">#ce2111</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushHalfWhite" Color="#55ffffff" />
|
||||
<SolidColorBrush x:Key="ColorBrushSemiWhite" Color="#bbffffff" />
|
||||
<SolidColorBrush x:Key="ColorBrushWhite" Color="#ffffff" />
|
||||
<SolidColorBrush x:Key="ColorBrushTransparent" Color="#00ffffff" />
|
||||
<SolidColorBrush x:Key="ColorBrushSemiTransparent">#01eaf2fe</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushTransparentBackground" Color="#d2fbfbfb" />
|
||||
<SolidColorBrush x:Key="ColorBrushBackground">#fbfbfb</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushToolTip">#e5ffffff</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushMemory">#000000</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushFatal">#c23616</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushError">#e74c3c</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushWarn">#f39c12</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushInfoDark">#ffffff</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushInfo">#000000</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ColorBrushDebug">#95a5a6</SolidColorBrush>
|
||||
|
||||
<!-- Window -->
|
||||
<Style TargetType="{x:Type Window}" x:Key="MyWindow">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Window}">
|
||||
<AdornerDecorator>
|
||||
<ContentPresenter />
|
||||
</AdornerDecorator>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Label -->
|
||||
<Style TargetType="Label">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Label">
|
||||
<Border Background="{TemplateBinding Background}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
RecognizesAccessKey="False"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="local:MyTextButton">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Label">
|
||||
<Border Background="{TemplateBinding Background}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Text="{TemplateBinding Content}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- TextBlock -->
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource LaunchFontFamily}" />
|
||||
</Style>
|
||||
<Style TargetType="TextBlock" x:Key="BasedOnTextBlock">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource LaunchFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<!-- ScrollViewer -->
|
||||
<Style x:Key="MyThumb" TargetType="Thumb">
|
||||
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Background="{DynamicResource ColorBrushSemiTransparent}" SnapsToDevicePixels="True">
|
||||
<Border Background="{TemplateBinding Foreground}" CornerRadius="3" Margin="2" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="local:MyScrollBar">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="False" />
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
<Setter Property="MinWidth" Value="8" />
|
||||
<Setter Property="Width" Value="8" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Border CornerRadius="2">
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="true"
|
||||
IsEnabled="{TemplateBinding IsMouseOver}">
|
||||
<Track.Resources>
|
||||
<sys:Double
|
||||
x:Key="{x:Static SystemParameters.VerticalScrollBarButtonHeightKey}">
|
||||
100
|
||||
</sys:Double>
|
||||
</Track.Resources>
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton Command="{x:Static ScrollBar.PageUpCommand}" Opacity="0" />
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton Command="{x:Static ScrollBar.PageDownCommand}" Opacity="0" />
|
||||
</Track.IncreaseRepeatButton> <!-- 不能删,删了点击 Thumb 以外的部分就不能翻页了 -->
|
||||
<Track.Thumb>
|
||||
<Thumb Style="{DynamicResource MyThumb}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="local:MyScrollViewer">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="CanContentScroll" Value="False" />
|
||||
<Setter Property="PanningMode" Value="VerticalOnly" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollViewer">
|
||||
<Grid>
|
||||
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
|
||||
CanContentScroll="{TemplateBinding CanContentScroll}"
|
||||
CanHorizontallyScroll="False" CanVerticallyScroll="False"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Content="{TemplateBinding Content}" Grid.Column="0"
|
||||
Margin="{TemplateBinding Padding}" Grid.Row="0" />
|
||||
<local:MyScrollBar x:Name="PART_VerticalScrollBar"
|
||||
AutomationProperties.AutomationId="VerticalScrollBar"
|
||||
Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}"
|
||||
Minimum="0" Grid.Row="0"
|
||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
|
||||
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Background="{TemplateBinding Background}"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="local:PageComp" BasedOn="{StaticResource {x:Type local:MyScrollViewer}}" />
|
||||
<Style TargetType="ScrollViewer" x:Key="ScrollViewerFullMargin">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="CanContentScroll" Value="False" />
|
||||
<Setter Property="PanningMode" Value="VerticalOnly" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollViewer">
|
||||
<Grid SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
Margin="{TemplateBinding Padding}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
|
||||
CanContentScroll="{TemplateBinding CanContentScroll}"
|
||||
CanHorizontallyScroll="False" CanVerticallyScroll="False"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Content="{TemplateBinding Content}" />
|
||||
<local:MyScrollBar Margin="2,1,1,1" x:Name="PART_VerticalScrollBar"
|
||||
AutomationProperties.AutomationId="VerticalScrollBar"
|
||||
Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}"
|
||||
Minimum="0" Grid.Row="0"
|
||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
|
||||
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Background="{TemplateBinding Background}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="ScrollViewer" x:Key="ScrollViewerNoMargin">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="CanContentScroll" Value="False" />
|
||||
<Setter Property="PanningMode" Value="VerticalOnly" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollViewer">
|
||||
<Grid SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
|
||||
Margin="{TemplateBinding Padding}" Cursor="Arrow">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
|
||||
CanContentScroll="{TemplateBinding CanContentScroll}"
|
||||
CanHorizontallyScroll="False" CanVerticallyScroll="False"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Content="{TemplateBinding Content}"
|
||||
Cursor="{TemplateBinding Cursor}" />
|
||||
<local:MyScrollBar x:Name="PART_VerticalScrollBar"
|
||||
AutomationProperties.AutomationId="VerticalScrollBar"
|
||||
Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}"
|
||||
Minimum="0" Grid.Row="0"
|
||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
|
||||
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Background="{TemplateBinding Background}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- TextBox -->
|
||||
<Style TargetType="local:MyTextBox">
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrushHalfWhite}" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource ColorBrush3}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource LaunchFontFamily}" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="IsUndoEnabled" Value="True" />
|
||||
<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="AllowDrop" Value="True" />
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
|
||||
<Setter Property="UndoLimit" Value="15" />
|
||||
<Setter Property="MaxLength" Value="1000" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ColorBrushBg0}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:MyTextBox">
|
||||
<Grid SnapsToDevicePixels="True" ToolTip="{TemplateBinding ToolTip}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition MinHeight="28" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border x:Name="border" BorderThickness="{TemplateBinding BorderThickness}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ScrollViewer x:Name="PART_ContentHost"
|
||||
Style="{DynamicResource ScrollViewerNoMargin}"
|
||||
RenderOptions.ClearTypeHint="Enabled" BorderThickness="0"
|
||||
Margin="5,0,0,0" Cursor="IBeam" />
|
||||
<!-- 由于未知原因,TextView 会有 2,0 的 Margin -->
|
||||
</Border>
|
||||
<Border Margin="{TemplateBinding Padding}" IsHitTestVisible="False"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
BorderThickness="0">
|
||||
<TextBlock x:Name="labHint" Foreground="{StaticResource ColorBrushGray4}"
|
||||
Margin="7,0,0,0" Padding="{TemplateBinding BorderThickness}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontFamily="{DynamicResource LaunchFontFamily}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Row="1" x:Name="labWrong" Height="0"
|
||||
Foreground="{TemplateBinding BorderBrush}" Padding="0,4,0,0"
|
||||
Margin="0,0,0,-3" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="behaviors:ClipboardInterceptor.EnableSafeClipboard" Value="True" />
|
||||
</Style>
|
||||
<Style TargetType="PasswordBox">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Top" />
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource ColorBrush3}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrushHalfWhite}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ColorBrushBg0}" />
|
||||
<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="PasswordBox">
|
||||
<Border x:Name="border" BorderThickness="1" BorderBrush="{TemplateBinding BorderBrush}"
|
||||
Background="{TemplateBinding Background}" Padding="0" SnapsToDevicePixels="True"
|
||||
CornerRadius="3">
|
||||
<local:MyScrollViewer x:Name="PART_ContentHost" Margin="4.5,4.5,0,4.5"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
RenderOptions.ClearTypeHint="Enabled" SnapsToDevicePixels="True" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Trigger.Setters>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ColorBrushGray5}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrushGray6}" />
|
||||
</Trigger.Setters>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Trigger.Setters>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ColorBrush4}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrush7}" />
|
||||
</Trigger.Setters>
|
||||
</Trigger>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Trigger.Setters>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ColorBrush3}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrush7}" />
|
||||
</Trigger.Setters>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ComboBox -->
|
||||
<DataTemplate x:Key="ComboBoxItemTemplateWithDelete">
|
||||
<local:MyListItem Title="{Binding}" FontSize="13" MaxHeight="26" IsScaleAnimationEnabled="False"
|
||||
Margin="-3,0,0,0">
|
||||
<local:MyListItem.Buttons>
|
||||
<x:Array Type="local:MyIconButton">
|
||||
<local:MyIconButton Height="20" Width="20" VerticalAlignment="Center" Tag="{Binding}"
|
||||
Click="_MyIconButtonClick"
|
||||
SvgIcon="lucide/x" />
|
||||
</x:Array>
|
||||
</local:MyListItem.Buttons>
|
||||
</local:MyListItem>
|
||||
</DataTemplate>
|
||||
<Style TargetType="local:MyComboBox">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrushBg0}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrushHalfWhite}" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="MaxDropDownHeight" Value="160" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<Border x:Name="border" Background="{TemplateBinding Background}" CornerRadius="3"
|
||||
BorderThickness="1" BorderBrush="{TemplateBinding Foreground}" />
|
||||
<ToggleButton Margin="0,0,1.5,0" Foreground="{TemplateBinding Foreground}"
|
||||
IsTabStop="False" SnapsToDevicePixels="False" Focusable="False"
|
||||
IsChecked="{Binding IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press" />
|
||||
<ContentPresenter x:Name="PART_Content" Margin="8.5,0,21.5,0" IsHitTestVisible="False"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" />
|
||||
<Popup x:Name="PART_Popup" SnapsToDevicePixels="True" VerticalOffset="-1.5"
|
||||
AllowsTransparency="True"
|
||||
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Placement="Bottom" PopupAnimation="Fade">
|
||||
<Grid x:Name="PanPopup">
|
||||
<Border Background="{DynamicResource ColorBrushWhite}" CornerRadius="3" />
|
||||
<Border x:Name="dropDownBorder" CornerRadius="3"
|
||||
BorderBrush="{TemplateBinding Foreground}" BorderThickness="1"
|
||||
Width="{TemplateBinding Width}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||
<local:MyScrollViewer x:Name="DropDownScrollViewer"
|
||||
SnapsToDevicePixels="False"
|
||||
Style="{DynamicResource ScrollViewerFullMargin}"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter x:Name="ItemsPresenter"
|
||||
KeyboardNavigation.DirectionalNavigation="Contained" />
|
||||
</local:MyScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="HasItems" Value="false">
|
||||
<Setter Property="Height" TargetName="dropDownBorder" Value="26" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEditable" Value="True">
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="ItemTemplate" Value="{DynamicResource ComboBoxItemTemplateWithDelete}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||
CornerRadius="3" BorderThickness="1"
|
||||
BorderBrush="{TemplateBinding Foreground}" />
|
||||
<ToggleButton Margin="0,0,1.5,0" Foreground="{TemplateBinding Foreground}"
|
||||
IsTabStop="False" SnapsToDevicePixels="False" Focusable="False"
|
||||
IsChecked="{Binding IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press" />
|
||||
<local:MyTextBox x:Name="PART_EditableTextBox" BorderThickness="0"
|
||||
Margin="1.5,0,21.5,0" Padding="0" Background="{x:Null}" />
|
||||
<Popup x:Name="PART_Popup" SnapsToDevicePixels="True" VerticalOffset="-1.5"
|
||||
AllowsTransparency="True"
|
||||
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Placement="Bottom" PopupAnimation="Fade">
|
||||
<Grid x:Name="PanPopup">
|
||||
<Border Background="{DynamicResource ColorBrushWhite}" CornerRadius="3" />
|
||||
<Border x:Name="dropDownBorder" SnapsToDevicePixels="False"
|
||||
CornerRadius="3" BorderBrush="{TemplateBinding Foreground}"
|
||||
BorderThickness="1" Width="{TemplateBinding Width}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||
<local:MyScrollViewer x:Name="DropDownScrollViewer"
|
||||
Style="{DynamicResource ScrollViewerFullMargin}"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter x:Name="ItemsPresenter"
|
||||
KeyboardNavigation.DirectionalNavigation="Contained" />
|
||||
</local:MyScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="HasItems" Value="false">
|
||||
<Setter Property="Height" TargetName="dropDownBorder" Value="26" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="ToggleButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border Background="{DynamicResource ColorBrushSemiTransparent}">
|
||||
<Path x:Name="path" Width="15" RenderTransformOrigin="0.3,0.5"
|
||||
HorizontalAlignment="Right" Data="M1,1L4.5,4.5L8,1" StrokeThickness="1.5"
|
||||
Stroke="{TemplateBinding Foreground}" VerticalAlignment="Center">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard Storyboard.TargetName="path"
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)">
|
||||
<DoubleAnimation To="180" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CircleEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard Storyboard.TargetName="path"
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)">
|
||||
<DoubleAnimation To="0" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CircleEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="ComboBoxItem" x:Key="MyComboBoxItem">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="ToolTipService.Placement" Value="Relative" />
|
||||
<Setter Property="ToolTipService.VerticalOffset" Value="-2" />
|
||||
<Setter Property="ToolTipService.HorizontalOffset" Value="200" />
|
||||
<Setter Property="ToolTipService.InitialShowDelay" Value="150" />
|
||||
<Setter Property="ToolTipService.BetweenShowDelay" Value="150" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrushTransparent}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border x:Name="ItemBorder" Padding="6,4" SnapsToDevicePixels="True"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border x:Name="ItemBorder" SnapsToDevicePixels="True"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ContextMenu -->
|
||||
<Style TargetType="ContextMenu">
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="HasDropShadow" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContextMenu">
|
||||
<Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1" CornerRadius="3"
|
||||
Background="{DynamicResource ColorBrushWhite}" Margin="0,0,4,4"
|
||||
SnapsToDevicePixels="True">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Opacity="0.4" BlurRadius="4" ShadowDepth="2" />
|
||||
</Border.Effect>
|
||||
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"
|
||||
Margin="0.5" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<!-- MenuItem -->
|
||||
<Style TargetType="local:MyMenuItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="MenuItem">
|
||||
<Border Name="PanBack" Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}" SnapsToDevicePixels="True" Height="26">
|
||||
<Grid SnapsToDevicePixels="False" UseLayoutRounding="False">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Icon" />
|
||||
<ColumnDefinition />
|
||||
<!-- <ColumnDefinition Width="Auto" SharedSizeGroup="Shortcut"/>
|
||||
<ColumnDefinition Width="13"/> -->
|
||||
</Grid.ColumnDefinitions>
|
||||
<Path Name="Icon" VerticalAlignment="Center" Stretch="Uniform" Margin="8,0"
|
||||
Fill="{TemplateBinding Foreground}" Height="12" Width="15" />
|
||||
<TextBlock Name="HeaderHost" Grid.Column="1" Text="{TemplateBinding Header}"
|
||||
Foreground="{TemplateBinding Foreground}" Margin="0,0,40,0"
|
||||
VerticalAlignment="Center" Height="16" />
|
||||
<!-- <TextBlock x:Name="InputGestureText" Grid.Column="2" Text="{TemplateBinding InputGestureText}" Margin="5,2,0,2" DockPanel.Dock="Right" /> -->
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="Icon" Value="{x:Null}">
|
||||
<Setter TargetName="Icon" Property="Visibility" Value="Hidden" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<!-- Calendar -->
|
||||
<Style TargetType="Calendar">
|
||||
<Setter Property="Background" Value="{DynamicResource ColorBrush8}" />
|
||||
</Style>
|
||||
<!-- BlurBorder -->
|
||||
<Style TargetType="controls:BlurBorder">
|
||||
<Setter Property="BlurRadius" Value="{DynamicResource BlurRadius}" />
|
||||
<Setter Property="BlurSamplingRate" Value="{DynamicResource BlurSamplingRate}" />
|
||||
<Setter Property="BlurKernelType" Value="{DynamicResource BlurType}" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Threading;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Essentials;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.UI.Controls;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.OS;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class Application
|
||||
{
|
||||
public Application()
|
||||
{
|
||||
// 注册生命周期事件
|
||||
Lifecycle.When(LifecycleState.Loaded, _ApplicationStartup);
|
||||
Lifecycle.When(LifecycleState.WindowCreated, _ShowEnvironmentWarning);
|
||||
SessionEnding += _ApplicationSessionEnding;
|
||||
}
|
||||
|
||||
// 开始
|
||||
private static void _ApplicationStartup()
|
||||
{
|
||||
try
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
// 创建自定义跟踪监听器,用于检测是否存在 Binding 失败
|
||||
PresentationTraceSources.DataBindingSource.Listeners.Add(new BindingErrorTraceListener());
|
||||
PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Error;
|
||||
Thread.CurrentThread.Priority = ThreadPriority.Highest;
|
||||
StartupValidation.EnsureWpfFont();
|
||||
|
||||
// 检查参数调用
|
||||
var args = Basics.CommandLineArguments;
|
||||
if (args.Length > 0)
|
||||
if (args[0] == "--gpu")
|
||||
// 调整显卡设置
|
||||
try
|
||||
{
|
||||
ModMain.SetGPUPreference(args[1].Trim('"'));
|
||||
Environment.Exit((int)ModBase.ProcessReturnValues.TaskDone);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Environment.Exit((int)ModBase.ProcessReturnValues.Fail);
|
||||
}
|
||||
|
||||
// 初始化文件结构
|
||||
Directory.CreateDirectory(ModBase.exePath + @"PCL\Pictures");
|
||||
Directory.CreateDirectory(ModBase.exePath + @"PCL\Musics");
|
||||
Directory.CreateDirectory(Path.Combine(ModBase.pathTemp, "Cache"));
|
||||
Directory.CreateDirectory(Path.Combine(ModBase.pathTemp, "Download"));
|
||||
Directory.CreateDirectory(ModBase.pathAppdata);
|
||||
|
||||
// 设置 ToolTipService 默认值
|
||||
ToolTipService.InitialShowDelayProperty.OverrideMetadata(typeof(DependencyObject),
|
||||
new FrameworkPropertyMetadata(100));
|
||||
Tooltip.Enable();
|
||||
|
||||
// 设置初始窗口
|
||||
if (Config.Preference.ShowStartupLogo)
|
||||
{
|
||||
ModMain.frmStart = new SplashScreen(@"Images\icon.ico");
|
||||
ModMain.frmStart.Show(false, true);
|
||||
}
|
||||
|
||||
// 设置初始化
|
||||
_ = Config.Debug.Enabled;
|
||||
_ = Config.Debug.AnimationSpeed;
|
||||
_ = Config.Network.HttpProxy.CustomAddress;
|
||||
_ = Config.Network.HttpProxy.CustomUsername;
|
||||
_ = Config.Network.HttpProxy.Type;
|
||||
_ = Config.Download.ThreadLimit;
|
||||
_ = Config.Download.SpeedLimit;
|
||||
_ = Config.Preference.Font;
|
||||
var updateBranchCfg = Config.Update.UpdateChannelConfig;
|
||||
if (updateBranchCfg.IsDefault())
|
||||
updateBranchCfg.SetValue(ModBase.versionBaseName.Contains("beta")
|
||||
? Core.App.UpdateChannel.Beta
|
||||
: Core.App.UpdateChannel.Release);
|
||||
|
||||
// 删除旧日志
|
||||
for (var i = 1; i <= 5; i++)
|
||||
{
|
||||
var oldLogFile = $@"{ModBase.exePath}PCL\Log-CE{i}.log";
|
||||
if (File.Exists(oldLogFile))
|
||||
File.Delete(oldLogFile);
|
||||
}
|
||||
|
||||
// 计时
|
||||
ModBase.Log("[Start] 第一阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.applicationStartTick) + " ms");
|
||||
ModBase.applicationStartTick = TimeUtils.GetTimeTick();
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var filePath = Basics.ExecutablePath;
|
||||
var summary = Lang.Text("Application.InitializationError.Path",
|
||||
string.IsNullOrEmpty(filePath)
|
||||
? Lang.Text("Application.InitializationError.PathUnavailable")
|
||||
: filePath);
|
||||
MessageBox.Show(
|
||||
ExceptionDetails.Compose(summary, ex),
|
||||
Lang.Text("SystemDialog.Startup.InitializationTitle"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
FormMain.EndProgramForce(ModBase.ProcessReturnValues.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
// 检测异常环境
|
||||
private static void _ShowEnvironmentWarning()
|
||||
{
|
||||
var problemList = new List<string>();
|
||||
var currentOsVersion = NtInterop.GetCurrentOsVersion();
|
||||
if (currentOsVersion.Build < 17763)
|
||||
problemList.Add(Lang.Text("Application.EnvironmentWarning.WindowsVersion"));
|
||||
if (SystemInfo.Is32BitSystem)
|
||||
problemList.Add(Lang.Text("Application.EnvironmentWarning.System32Bit"));
|
||||
if (ModBase.exePath.Contains(Path.GetTempPath()) || ModBase.exePath.Contains(@"AppData\Local\Temp\"))
|
||||
problemList.Add(Lang.Text("Application.EnvironmentWarning.TempFolder"));
|
||||
if (ModBase.exePath.ContainsF("wechat_files", true) || ModBase.exePath.ContainsF("WeChat Files", true) ||
|
||||
ModBase.exePath.ContainsF("Tencent Files", true))
|
||||
problemList.Add(Lang.Text("Application.EnvironmentWarning.SocialSoftwareFolder"));
|
||||
if (problemList.Count == 0) return;
|
||||
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Application.EnvironmentWarning.Message", problemList.Join("\r\n")),
|
||||
Lang.Text("Application.EnvironmentWarning.Title"),
|
||||
Lang.Text("Application.EnvironmentWarning.IKnow"),
|
||||
isWarn: true);
|
||||
}
|
||||
|
||||
// 结束
|
||||
private static void _ApplicationSessionEnding(object sender, SessionEndingCancelEventArgs e)
|
||||
{
|
||||
ModMain.frmMain.EndProgram(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling for unhandled exceptions
|
||||
*/
|
||||
private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.Handled = true;
|
||||
if (ModBase.isProgramEnded) return;
|
||||
|
||||
ModBase.FeedbackInfo();
|
||||
|
||||
var detail = e.Exception.ToString();
|
||||
|
||||
// Automatic error analysis for environment issues
|
||||
if (detail.Contains("System.Windows.Threading.Dispatcher.Invoke") ||
|
||||
detail.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") ||
|
||||
detail.Contains("未能加载文件或程序集"))
|
||||
{
|
||||
ModBase.OpenWebsite("https://get.dot.net/10");
|
||||
LogWrapper.Error(
|
||||
e.Exception,
|
||||
Lang.Text("SystemDialog.Startup.DotNetRuntimeOutdated.Message"));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWrapper.Error(e.Exception, Lang.Text("SystemDialog.Error.Unexpected.Message"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Equivalent to On Error Resume Next for safety in the global handler
|
||||
}
|
||||
}
|
||||
|
||||
// Win32 API declaration for DLL directory configuration
|
||||
[DllImport("kernel32", EntryPoint = "SetDllDirectoryA", CharSet = CharSet.Ansi)]
|
||||
private static extern bool _SetDllDirectory(string lpPathName);
|
||||
// 切换窗口
|
||||
|
||||
// 控件模板事件
|
||||
private void _MyIconButtonClick(object sender, EventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
// 自定义监听器类
|
||||
public class BindingErrorTraceListener : TraceListener
|
||||
{
|
||||
public override void Write(string message)
|
||||
{
|
||||
ModBase.Log($"警告,检测到 Binding 失败:{message}");
|
||||
}
|
||||
|
||||
public override void WriteLine(string message)
|
||||
{
|
||||
ModBase.Log($"警告,检测到 Binding 失败:{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class AnimatedBackgroundGrid : Grid
|
||||
{
|
||||
public static readonly DependencyProperty BackgroundBrushProperty = DependencyProperty.Register("BackgroundBrush",
|
||||
typeof(SolidColorBrush), typeof(AnimatedBackgroundGrid),
|
||||
new PropertyMetadata(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), _BackgroundBrushChanged));
|
||||
|
||||
private readonly DependencyProperty _animatableBrushProperty;
|
||||
|
||||
public readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public AnimatedBackgroundGrid(DependencyProperty brushDp)
|
||||
{
|
||||
_animatableBrushProperty = brushDp;
|
||||
Loaded += (_, _) => Init();
|
||||
}
|
||||
|
||||
public AnimatedBackgroundGrid() : this(BackgroundProperty)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual FrameworkElement AnimatableElement => this;
|
||||
|
||||
protected virtual SolidColorBrush AnimatableBrush
|
||||
{
|
||||
get => (SolidColorBrush)Background;
|
||||
set => Background = value;
|
||||
}
|
||||
|
||||
protected bool IsAnimating
|
||||
{
|
||||
get => field;
|
||||
private set => field = value;
|
||||
}
|
||||
|
||||
public SolidColorBrush BackgroundBrush
|
||||
{
|
||||
get => (SolidColorBrush)GetValue(BackgroundBrushProperty);
|
||||
set => SetValue(BackgroundBrushProperty, value);
|
||||
}
|
||||
|
||||
private static void _BackgroundBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var grid = (AnimatedBackgroundGrid)d;
|
||||
var brush = (SolidColorBrush)e.NewValue;
|
||||
if (!(grid.IsLoaded && grid.IsVisible))
|
||||
{
|
||||
grid.AnimatableBrush = brush;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||||
{
|
||||
grid.IsAnimating = true;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(grid.AnimatableElement, grid._animatableBrushProperty,
|
||||
new ModBase.MyColor(brush) - grid.AnimatableBrush, 300)
|
||||
}, "MyCard Theme " + grid.uuid);
|
||||
await Task.Delay(300);
|
||||
grid.AnimatableBrush = brush;
|
||||
grid.IsAnimating = false;
|
||||
}));
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
AnimatableBrush = BackgroundBrush;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using Clipboard = System.Windows.Forms.Clipboard;
|
||||
|
||||
// Author: uye (owner of the MaaAssistantArknights team)
|
||||
// Original Source: MaaAssistantArknights project - https://github.com/MaaAssistantArknights/MaaAssistantArknights
|
||||
// License: Apache License 2.0 (this file only)
|
||||
//
|
||||
// This file is based on work originally developed in the MaaAssistantArknights project,
|
||||
// which is licensed under the GNU AGPL v3.0 only.
|
||||
//
|
||||
// As the original author of this code, I am re-licensing this specific file under
|
||||
// the Apache License 2.0 for use in PCL2-CE.
|
||||
//
|
||||
// Description:
|
||||
// Implements a WPF clipboard fix to handle OpenClipboard failures in TextBox,
|
||||
// RichTextBox, and DataGrid, typically caused by focus issues or external hooks.
|
||||
//
|
||||
// Date: 2025-07-03
|
||||
|
||||
namespace PCL.Controls.Behaviors;
|
||||
|
||||
public sealed class ClipboardInterceptor
|
||||
{
|
||||
public static readonly DependencyProperty EnableSafeClipboardProperty =
|
||||
DependencyProperty.RegisterAttached("EnableSafeClipboard", typeof(bool), typeof(ClipboardInterceptor),
|
||||
new PropertyMetadata(false, OnEnableSafeClipboardChanged));
|
||||
|
||||
private ClipboardInterceptor()
|
||||
{
|
||||
}
|
||||
|
||||
public static void SetEnableSafeClipboard(DependencyObject element, bool value)
|
||||
{
|
||||
element.SetValue(EnableSafeClipboardProperty, value);
|
||||
}
|
||||
|
||||
public static bool GetEnableSafeClipboard(DependencyObject element)
|
||||
{
|
||||
return (bool)element.GetValue(EnableSafeClipboardProperty);
|
||||
}
|
||||
|
||||
private static void OnEnableSafeClipboardChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(bool)e.NewValue)
|
||||
return;
|
||||
|
||||
switch (d)
|
||||
{
|
||||
case TextBox box:
|
||||
AddCommandBindingsToTextBox(box);
|
||||
break;
|
||||
case RichTextBox box:
|
||||
AddCommandBindingsToRichTextBox(box);
|
||||
break;
|
||||
case DataGrid grid:
|
||||
AddCommandBindingsToDataGrid(grid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCommandBindingsToTextBox(TextBox tb)
|
||||
{
|
||||
tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyTextBox));
|
||||
tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutTextBox));
|
||||
tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteTextBox));
|
||||
}
|
||||
|
||||
private static void AddCommandBindingsToRichTextBox(RichTextBox rtb)
|
||||
{
|
||||
rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyRichTextBox));
|
||||
rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutRichTextBox));
|
||||
rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteRichTextBox));
|
||||
}
|
||||
|
||||
private static void AddCommandBindingsToDataGrid(DataGrid dg)
|
||||
{
|
||||
dg.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyDataGrid));
|
||||
}
|
||||
|
||||
private static void OnCopyTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var tb = sender as TextBox;
|
||||
if (tb is null || tb.SelectionLength <= 0)
|
||||
return;
|
||||
|
||||
TrySetClipboardText(tb.SelectedText);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnCutTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var tb = sender as TextBox;
|
||||
if (tb is null || tb.SelectionLength <= 0)
|
||||
return;
|
||||
|
||||
TrySetClipboardText(tb.SelectedText);
|
||||
|
||||
tb.SelectedText = string.Empty;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnPasteTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var tb = sender as TextBox;
|
||||
if (tb is null)
|
||||
return;
|
||||
|
||||
if (Clipboard.ContainsText())
|
||||
{
|
||||
var pasteText = Clipboard.GetText();
|
||||
|
||||
if (!tb.AcceptsReturn)
|
||||
pasteText = pasteText.Replace("\r\n", " ").Replace("\r", " ")
|
||||
.Replace("\n", " ");
|
||||
|
||||
var start = tb.SelectionStart;
|
||||
|
||||
tb.SelectedText = pasteText;
|
||||
tb.CaretIndex = start + pasteText.Length;
|
||||
tb.SelectionLength = 0;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnCopyRichTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var rtb = sender as RichTextBox;
|
||||
if (rtb is null)
|
||||
return;
|
||||
|
||||
var textRange = new TextRange(rtb.Selection.Start, rtb.Selection.End);
|
||||
if (string.IsNullOrEmpty(textRange.Text))
|
||||
return;
|
||||
|
||||
TrySetClipboardText(textRange.Text);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnCutRichTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var rtb = sender as RichTextBox;
|
||||
if (rtb is null)
|
||||
return;
|
||||
|
||||
var selection = new TextRange(rtb.Selection.Start, rtb.Selection.End);
|
||||
if (string.IsNullOrEmpty(selection.Text))
|
||||
return;
|
||||
|
||||
TrySetClipboardText(selection.Text);
|
||||
|
||||
selection.Text = string.Empty;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnPasteRichTextBox(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var rtb = sender as RichTextBox;
|
||||
if (rtb is null)
|
||||
return;
|
||||
|
||||
if (!Clipboard.ContainsText())
|
||||
return;
|
||||
|
||||
var pasteText = Clipboard.GetText();
|
||||
var selection = rtb.Selection;
|
||||
|
||||
selection.Text = pasteText;
|
||||
|
||||
var caretPos = selection.End;
|
||||
rtb.CaretPosition = caretPos;
|
||||
rtb.Selection.Select(caretPos, caretPos);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnCopyDataGrid(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var dg = sender as DataGrid;
|
||||
if (dg is null || dg.SelectedCells is null || dg.SelectedCells.Count == 0)
|
||||
return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var rowGroups = dg.SelectedCells.GroupBy(c => c.Item);
|
||||
|
||||
foreach (var row in rowGroups)
|
||||
{
|
||||
var rowText = string.Join("\t", row.Select(cell =>
|
||||
{
|
||||
var tb = cell.Column.GetCellContent(cell.Item) as TextBlock;
|
||||
return tb is not null ? tb.Text : "";
|
||||
}));
|
||||
sb.AppendLine(rowText);
|
||||
}
|
||||
|
||||
TrySetClipboardText(sb.ToString().TrimEnd('\r', '\n'));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static bool TrySetClipboardText(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.Clear();
|
||||
Clipboard.SetDataObject(text, true);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal static class LazyLoader
|
||||
{
|
||||
public static void EnableLazyLoad(this FrameworkElement element, Action action)
|
||||
{
|
||||
var behavior = new LazyLoadBehavior();
|
||||
behavior.Action = action;
|
||||
Interaction.GetBehaviors(element).Add(behavior);
|
||||
}
|
||||
}
|
||||
|
||||
public class LazyLoadBehavior : Behavior<FrameworkElement>
|
||||
{
|
||||
public static readonly DependencyProperty ActionProperty = DependencyProperty.Register(nameof(Action),
|
||||
typeof(Action), typeof(LazyLoadBehavior), new PropertyMetadata(null));
|
||||
|
||||
public Action Action
|
||||
{
|
||||
get => (Action)GetValue(ActionProperty);
|
||||
set => SetValue(ActionProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnLayoutUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var element = AssociatedObject;
|
||||
if (element is null || element.RenderSize.Width < double.Epsilon || !element.IsVisible)
|
||||
return;
|
||||
|
||||
var scrollViewer = FindParentScrollViewer(element);
|
||||
if (scrollViewer is null)
|
||||
return;
|
||||
|
||||
var elementBounds = element.TransformToAncestor(scrollViewer)
|
||||
.TransformBounds(new Rect(new Point(0d, 0d), element.RenderSize));
|
||||
var viewport = new Rect(0d, 0d, scrollViewer.ViewportWidth, scrollViewer.ViewportHeight);
|
||||
|
||||
if (!viewport.IntersectsWith(elementBounds))
|
||||
return;
|
||||
|
||||
Action?.Invoke();
|
||||
// 仅执行一次
|
||||
element.LayoutUpdated -= OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static ScrollViewer FindParentScrollViewer(DependencyObject d)
|
||||
{
|
||||
for (var current = d; current is not null; current = VisualTreeHelper.GetParent(current))
|
||||
{
|
||||
if (current is ScrollViewer scrollViewer)
|
||||
return scrollViewer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal static class ControlVisualHelpers
|
||||
{
|
||||
internal static bool ShouldAnimate(FrameworkElement control, object? animationOverride = null)
|
||||
{
|
||||
return control.IsLoaded && ModAnimation.AniControlEnabled == 0 && !false.Equals(animationOverride);
|
||||
}
|
||||
|
||||
internal static void AnimateColorOrSetResource(FrameworkElement target, DependencyProperty property,
|
||||
string resourceKey, int duration, string animationKey, bool shouldAnimate)
|
||||
{
|
||||
if (shouldAnimate)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(target, property, resourceKey, duration), animationKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModAnimation.AniStop(animationKey);
|
||||
target.SetResourceReference(property, resourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<ContentControl x:Class="PCL.FontSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="28" d:DesignWidth="800"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}">
|
||||
<local:MyComboBox x:Name="ComboFont" ItemsSource="{Binding CustomFontCollection}"
|
||||
MaxDropDownHeight="300" ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<local:MyComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" TextAlignment="Center"
|
||||
FontFamily="{Binding Font}" />
|
||||
</DataTemplate>
|
||||
</local:MyComboBox.ItemTemplate>
|
||||
<local:MyComboBox.ItemContainerStyle>
|
||||
<Style TargetType="local:MyComboBoxItem">
|
||||
<Setter Property="Tag" Value="{Binding Tag}" />
|
||||
</Style>
|
||||
</local:MyComboBox.ItemContainerStyle>
|
||||
</local:MyComboBox>
|
||||
</ContentControl>
|
||||
@@ -0,0 +1,234 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class FontSelector
|
||||
{
|
||||
public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e);
|
||||
|
||||
public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register(nameof(Tooltip),
|
||||
typeof(string), typeof(FontSelector), new PropertyMetadata(null, OnTooltipChanged));
|
||||
|
||||
private bool _isInitializing;
|
||||
private bool _isListeningLanguageChanged;
|
||||
private string? _pendingFontTag;
|
||||
|
||||
public FontSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += FontSelector_Loaded;
|
||||
Unloaded += FontSelector_Unloaded;
|
||||
ComboFont.SelectionChanged += ComboFont_SelectionChanged;
|
||||
}
|
||||
|
||||
public string Tooltip
|
||||
{
|
||||
get => (string)GetValue(TooltipProperty);
|
||||
set => SetValue(TooltipProperty, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<CustomFontProperties> CustomFontCollection { get; } = [];
|
||||
|
||||
public string SelectedFontTag
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ComboFont.SelectedItem is null) return "";
|
||||
return ComboFont.SelectedItem is not CustomFontProperties selectedFont ? "" : selectedFont.Tag;
|
||||
}
|
||||
set
|
||||
{
|
||||
// 如果字体还在加载中,延迟设置
|
||||
if (CustomFontCollection.Count == 0 ||
|
||||
(CustomFontCollection is [{ Name: var name }] && name == Lang.Text("Common.State.Loading")))
|
||||
{
|
||||
_pendingFontTag = value;
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitializing = true;
|
||||
|
||||
var targetSelection = CustomFontCollection.FirstOrDefault(i => i.Tag == value);
|
||||
if (targetSelection is null)
|
||||
ComboFont.SelectedIndex = 0;
|
||||
else
|
||||
ComboFont.SelectedItem = targetSelection;
|
||||
|
||||
_isInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => ComboFont.SelectedIndex;
|
||||
set => ComboFont.SelectedIndex = value;
|
||||
}
|
||||
|
||||
public new bool IsEnabled
|
||||
{
|
||||
get => ComboFont.IsEnabled;
|
||||
set => ComboFont.IsEnabled = value;
|
||||
}
|
||||
|
||||
private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is FontSelector control) control.ComboFont.ToolTip = e.NewValue;
|
||||
}
|
||||
|
||||
public event SelectionChangedEventHandler? SelectionChanged;
|
||||
|
||||
private void FontSelector_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StartListeningLanguageChanged();
|
||||
|
||||
if (CustomFontCollection.Count == 0)
|
||||
LoadFonts();
|
||||
else
|
||||
RefreshDefaultFont();
|
||||
}
|
||||
|
||||
private void FontSelector_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StopListeningLanguageChanged();
|
||||
}
|
||||
|
||||
private void StartListeningLanguageChanged()
|
||||
{
|
||||
if (_isListeningLanguageChanged) return;
|
||||
LocalizationService.LanguageChanged += LocalizationService_LanguageChanged;
|
||||
_isListeningLanguageChanged = true;
|
||||
}
|
||||
|
||||
private void StopListeningLanguageChanged()
|
||||
{
|
||||
if (!_isListeningLanguageChanged) return;
|
||||
LocalizationService.LanguageChanged -= LocalizationService_LanguageChanged;
|
||||
_isListeningLanguageChanged = false;
|
||||
}
|
||||
|
||||
private void LocalizationService_LanguageChanged()
|
||||
{
|
||||
RefreshDefaultFont();
|
||||
}
|
||||
|
||||
private void RefreshDefaultFont()
|
||||
{
|
||||
if (!Dispatcher.CheckAccess())
|
||||
{
|
||||
Dispatcher.Invoke(RefreshDefaultFont);
|
||||
return;
|
||||
}
|
||||
|
||||
GetDefaultFont()?.Font = LocalizationFontService.BuildLaunchFontFamily();
|
||||
}
|
||||
|
||||
private void LoadFonts()
|
||||
{
|
||||
Dispatcher.BeginInvoke(async () =>
|
||||
{
|
||||
ComboFont.IsEnabled = false;
|
||||
_isInitializing = true;
|
||||
CustomFontCollection.Add(new CustomFontProperties { Name = Lang.Text("Common.State.Loading") });
|
||||
ComboFont.SelectedIndex = 0;
|
||||
|
||||
var availableFonts = new List<(string Name, FontFamily Font)>();
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var font in Fonts.SystemFontFamilies)
|
||||
try
|
||||
{
|
||||
if (font.Source.StartsWith("Global ")) continue;
|
||||
|
||||
foreach (var typeface in font.GetTypefaces())
|
||||
{
|
||||
if (!typeface.TryGetGlyphTypeface(out var glyph))
|
||||
throw new NullReferenceException(
|
||||
$"字形 {typeface.FaceNames.GetForCurrentUiCulture("(unknown)")} 无法加载");
|
||||
|
||||
_ = new GlyphTypeface(glyph.FontUri);
|
||||
}
|
||||
|
||||
availableFonts.Add((font.FamilyNames.GetForCurrentUiCulture(), font));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, $"发现了一个无法加载的异常的字体:{font.Source}");
|
||||
}
|
||||
|
||||
availableFonts.Sort((l, r) => string.Compare(l.Name, r.Name, StringComparison.Ordinal));
|
||||
});
|
||||
|
||||
CustomFontCollection.Clear();
|
||||
CustomFontCollection.Add(new CustomFontProperties
|
||||
{
|
||||
Name = Lang.Text("Common.Option.Default"),
|
||||
Font = LocalizationFontService.BuildLaunchFontFamily(),
|
||||
Tag = ""
|
||||
});
|
||||
|
||||
foreach (var font in availableFonts)
|
||||
CustomFontCollection.Add(new CustomFontProperties
|
||||
{ Name = font.Name, Font = font.Font, Tag = font.Font.Source });
|
||||
|
||||
ComboFont.IsEnabled = true;
|
||||
|
||||
if (_pendingFontTag is not null)
|
||||
{
|
||||
var pendingTag = _pendingFontTag;
|
||||
_pendingFontTag = null;
|
||||
SelectedFontTag = pendingTag;
|
||||
}
|
||||
|
||||
_isInitializing = false;
|
||||
});
|
||||
}
|
||||
|
||||
private void ComboFont_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!_isInitializing) SelectionChanged?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private CustomFontProperties? GetDefaultFont()
|
||||
{
|
||||
return CustomFontCollection.FirstOrDefault(i => string.IsNullOrEmpty(i.Tag));
|
||||
}
|
||||
|
||||
public class CustomFontProperties : INotifyPropertyChanged
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
init => SetField(ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public FontFamily Font
|
||||
{
|
||||
get;
|
||||
set => SetField(ref field, value);
|
||||
} = LocalizationFontService.BuildLaunchFontFamily();
|
||||
|
||||
public string Tag
|
||||
{
|
||||
get;
|
||||
set => SetField(ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return;
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PCL;
|
||||
|
||||
public interface IMyRadio
|
||||
{
|
||||
delegate void ChangedEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
delegate void CheckEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
event CheckEventHandler Check;
|
||||
event ChangedEventHandler Changed;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Grid x:Class="PCL.MinecraftServer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" Margin="6" x:Name="ImgServerLogo" Width="54" Height="54" />
|
||||
<TextBlock Grid.Column="1" Margin="6" x:Name="LabServerDesc" TextWrapping="Wrap" FontSize="15" Foreground="White" />
|
||||
<controls:MotdRenderer Grid.Column="1" x:Name="MotdRenderer" Margin="0,20,3,1"
|
||||
IsHitTestVisible="False" Width="300" Height="Auto" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="2" Margin="6" HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="LabServerPlayer" HorizontalAlignment="Right" TextAlignment="Right"
|
||||
FontSize="12" Foreground="White" />
|
||||
<TextBlock x:Name="LabServerLatency" HorizontalAlignment="Right" TextAlignment="Right"
|
||||
FontSize="12" Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Link.McPing;
|
||||
using PCL.Core.Link.McPing.Model;
|
||||
using PCL.Core.Minecraft;
|
||||
using PCL.Core.UI;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MinecraftServer : Grid
|
||||
{
|
||||
private const string fallbackImageUri =
|
||||
"pack://application:,,,/Plain Craft Launcher 2;component/Images/Icons/DefaultServer.png";
|
||||
|
||||
private static readonly DependencyProperty AddressProperty = DependencyProperty.Register(nameof(Address),
|
||||
typeof(string), typeof(MinecraftServer), new PropertyMetadata(string.Empty, OnAddressChanged));
|
||||
|
||||
public MinecraftServer()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public string Address
|
||||
{
|
||||
get => (string)(GetValue(AddressProperty));
|
||||
set => SetValue(AddressProperty, value);
|
||||
}
|
||||
|
||||
private static void OnAddressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var server = (MinecraftServer)d;
|
||||
d.Dispatcher.BeginInvoke(new Func<Task>(() => server.UpdateServerInfoAsync(e.NewValue?.ToString())));
|
||||
}
|
||||
|
||||
public async Task UpdateServerInfoAsync(string address)
|
||||
{
|
||||
if (address is null)
|
||||
return;
|
||||
address = address.Replace(":", ":");
|
||||
// 预先重置UI状态
|
||||
LabServerDesc.Foreground = Brushes.White;
|
||||
LabServerDesc.Text = Lang.Text("Tools.ServerQuery.State.Querying");
|
||||
LabServerPlayer.Text = "-/-";
|
||||
LabServerPlayer.ToolTip = null;
|
||||
LabServerLatency.Text = string.Empty;
|
||||
ImageLoaderHelper.SetFallbackImage(ImgServerLogo, fallbackImageUri);
|
||||
|
||||
try
|
||||
{
|
||||
// 获取可达地址(DNS解析)
|
||||
var addr = await ServerAddressResolver.GetResolvedServerAddressAsync(address);
|
||||
|
||||
// Ping服务器
|
||||
using (var query = McPingServiceFactory.CreateService(addr.Host, addr.Ip, addr.Port))
|
||||
{
|
||||
var ret = await query.PingAsync();
|
||||
|
||||
if (ret is null) throw new Exception(Lang.Text("Tools.ServerQuery.State.NoInfo"));
|
||||
|
||||
// 处理服务器图标
|
||||
await ImageLoaderHelper.SetServerLogoAsync(ret.Favicon, ImgServerLogo);
|
||||
|
||||
// 更新UI
|
||||
UpdateServerStatus(ret);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[MinecraftServer] 信息查询失败");
|
||||
LabServerDesc.Text = Lang.Text("Tools.ServerQuery.Error.UnableToConnect", ex.Message);
|
||||
LabServerDesc.Foreground = Brushes.Red;
|
||||
ImageLoaderHelper.SetFallbackImage(ImgServerLogo, fallbackImageUri);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateServerStatus(McPingResult ret)
|
||||
{
|
||||
// 延迟颜色判断
|
||||
var latencyColor = ret.Latency < 150 ? "a" : ret.Latency < 400 ? "6" : "c";
|
||||
|
||||
// 更新描述
|
||||
LabServerDesc.Text = Lang.Text("Tools.ServerQuery.Title.MinecraftServer");
|
||||
MotdRenderer.RenderMotd(ret.Description, false, 2, 14);
|
||||
MotdRenderer.RenderCanvas();
|
||||
|
||||
// 更新玩家信息
|
||||
LabServerPlayer.Text = $"{ret.Players.Online}/{ret.Players.Max}";
|
||||
ModStyle.MinecraftFormatter.SetColorfulTextLab($"§{latencyColor}{ret.Latency}ms", LabServerLatency, false);
|
||||
|
||||
// 玩家列表提示
|
||||
if (ret.Players.Samples.Any())
|
||||
{
|
||||
LabServerPlayer.ToolTip = string.Join("\r\n", ret.Players.Samples.Select(x => x.Name));
|
||||
ToolTipService.SetPlacement(LabServerPlayer, PlacementMode.Mouse);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Grid x:Class="PCL.MinecraftServerQuery"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
xmlns:val="https://ce.pclc.cc/core/utils/validate"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:MyTextBox x:Name="LabServerIp" HintText="{DynamicResource Tools.ServerQuery.Address.Hint}" Margin="0,0,15,0" Grid.Column="0">
|
||||
<local:MyTextBox.ValidateRules>
|
||||
<val:BlacklistValidator>
|
||||
<val:BlacklistValidator.Blacklist>
|
||||
<sys:String>/</sys:String>
|
||||
</val:BlacklistValidator.Blacklist>
|
||||
</val:BlacklistValidator>
|
||||
</local:MyTextBox.ValidateRules>
|
||||
</local:MyTextBox>
|
||||
<local:MyButton x:Name="BtnServerQuery" Text="{DynamicResource Tools.ServerQuery.Action.Query}" Width="60" Grid.Column="1" />
|
||||
<Border x:Name="ServerInfo" Margin="0,7,0,0" Visibility="Collapsed" Grid.Row="1" Grid.ColumnSpan="2"
|
||||
CornerRadius="5" BorderThickness="2" BorderBrush="{DynamicResource ColorBrush5}">
|
||||
<Border.Background>
|
||||
<ImageBrush ImageSource="/Plain Craft Launcher 2;component/Images/Backgrounds/server_bg.png"
|
||||
Stretch="Uniform" TileMode="Tile" Viewport="0,0,124,124" ViewportUnits="Absolute" />
|
||||
</Border.Background>
|
||||
<local:MinecraftServer x:Name="PanMcServer" />
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MinecraftServerQuery : Grid
|
||||
{
|
||||
public MinecraftServerQuery()
|
||||
{
|
||||
InitializeComponent();
|
||||
BtnServerQuery.Click += BtnServerQuery_Click;
|
||||
}
|
||||
private void BtnServerQuery_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Func<Task>(() => ServerQueryAsync()));
|
||||
}
|
||||
|
||||
private async Task ServerQueryAsync()
|
||||
{
|
||||
await PanMcServer.UpdateServerInfoAsync(LabServerIp.Text);
|
||||
ServerInfo.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Border x:Class="PCL.MyButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
FocusVisualStyle="{x:Null}" x:Name="PanBack"
|
||||
Focusable="True" UseLayoutRounding="False" SnapsToDevicePixels="False"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}">
|
||||
<Border CornerRadius="3" x:Name="PanFore" BorderThickness="1" BorderBrush="{DynamicResource ColorBrush1}"
|
||||
RenderTransformOrigin="0.5,0.5" Background="{DynamicResource ColorBrushHalfWhite}">
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Border.RenderTransform>
|
||||
<TextBlock TextOptions.TextFormattingMode="Display" SnapsToDevicePixels="True" TextTrimming="None"
|
||||
HorizontalAlignment="Center" x:Name="LabText" VerticalAlignment="Center"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Foreground="{Binding Parent.BorderBrush, RelativeSource={RelativeSource Self}}" FontSize="13"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</Border>
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyButton
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
|
||||
|
||||
public enum ColorState
|
||||
{
|
||||
Normal = 0,
|
||||
Highlight = 1,
|
||||
Red = 2
|
||||
}
|
||||
|
||||
// 自定义事件
|
||||
private const int animationColorIn = 100;
|
||||
private const int animationColorOut = 200;
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyButton), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyButton)sender).LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
// 属性穿透
|
||||
public new static readonly DependencyProperty PaddingProperty = DependencyProperty.Register("Padding",
|
||||
typeof(Thickness), typeof(MyButton), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyButton)sender).PanFore.Padding = (Thickness)e.NewValue;
|
||||
}));
|
||||
|
||||
private ColorState _ColorType = ColorState.Normal; // 配色方案
|
||||
|
||||
// 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
|
||||
|
||||
|
||||
// 自定义属性
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MouseEnter += RefreshColor;
|
||||
MouseLeave += RefreshColor;
|
||||
Loaded += RefreshColor;
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseLeftButtonUp += Button_MouseUp;
|
||||
MouseLeftButtonDown += Button_MouseDown;
|
||||
MouseEnter += (_, _) => Button_MouseEnter();
|
||||
MouseLeftButtonUp += (_, _) => Button_MouseUp();
|
||||
MouseLeave += (_, _) => Button_MouseLeave();
|
||||
}
|
||||
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
} // 显示文本
|
||||
|
||||
public Thickness TextPadding
|
||||
{
|
||||
get => LabText.Padding;
|
||||
set => LabText.Padding = value;
|
||||
}
|
||||
|
||||
public ColorState ColorType
|
||||
{
|
||||
get => _ColorType;
|
||||
set
|
||||
{
|
||||
_ColorType = value;
|
||||
RefreshColor();
|
||||
}
|
||||
}
|
||||
|
||||
public new Thickness Padding
|
||||
{
|
||||
get => PanFore.Padding;
|
||||
set => PanFore.Padding = value;
|
||||
}
|
||||
|
||||
public Transform RealRenderTransform
|
||||
{
|
||||
get => PanFore.RenderTransform;
|
||||
set => PanFore.RenderTransform = value;
|
||||
}
|
||||
|
||||
// 声明
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
private string GetBorderBrushResourceKey()
|
||||
{
|
||||
return ColorType switch
|
||||
{
|
||||
ColorState.Normal => IsMouseOver ? "ColorBrush3" : "ColorBrush1",
|
||||
ColorState.Highlight => IsMouseOver ? "ColorBrush3" : "ColorBrush2",
|
||||
ColorState.Red => IsMouseOver ? "ColorBrushRedLight" : "ColorBrushRedDark",
|
||||
_ => "ColorBrush1"
|
||||
};
|
||||
}
|
||||
|
||||
private void StartBorderBrushAnimation(string resourceKey, int duration)
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(PanFore, BorderBrushProperty, resourceKey, duration)
|
||||
}, "MyButton Color " + Uuid);
|
||||
}
|
||||
|
||||
private void RefreshColor(object obj = null, object e = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
if (IsEnabled)
|
||||
StartBorderBrushAnimation(GetBorderBrushResourceKey(), IsMouseOver ? animationColorIn : animationColorOut);
|
||||
else
|
||||
// 不可用(Gray 4)
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(PanFore, BorderBrushProperty,
|
||||
ThemeManager.colorGray4 - PanFore.BorderBrush, animationColorOut)
|
||||
}, "MyButton Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModAnimation.AniStop("MyButton Color " + Uuid);
|
||||
if (IsEnabled)
|
||||
PanFore.SetResourceReference(BorderBrushProperty, GetBorderBrushResourceKey());
|
||||
else
|
||||
PanFore.BorderBrush = ThemeManager.colorGray4;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新按钮颜色出错");
|
||||
}
|
||||
}
|
||||
|
||||
// 实现自定义事件
|
||||
private bool isMouseDown = false;
|
||||
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下按钮:" + Text);
|
||||
Click?.Invoke(sender, e);
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isMouseDown = true;
|
||||
Focus();
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanFore, 0.955d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 80,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
ModAnimation.AaScaleTransform(PanFore, -0.01d, 700, ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void Button_MouseEnter()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanFore, BackgroundProperty,
|
||||
_ColorType == ColorState.Red ? "ColorBrushRedBack" : "ColorBrush7", animationColorIn),
|
||||
"MyButton Background " + Uuid);
|
||||
}
|
||||
|
||||
private void Button_MouseUp()
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
isMouseDown = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 300, 10,
|
||||
new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void Button_MouseLeave()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanFore, BackgroundProperty, "ColorBrushHalfWhite", animationColorOut),
|
||||
"MyButton Background " + Uuid);
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
isMouseDown = false;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 800,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), "MyButton Scale " + Uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyCard : AnimatedBackgroundGrid
|
||||
{
|
||||
// 动画
|
||||
private const double dropShadowIdleOpacity = 0.07d;
|
||||
private const double dropShadowHoverOpacity = 0.4d;
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register("Title", typeof(string), typeof(MyCard), new PropertyMetadata(""));
|
||||
|
||||
private readonly BlurBorder mainBorder;
|
||||
|
||||
// 控件
|
||||
private readonly Grid mainGrid;
|
||||
private TextBlock _MainTextBlock;
|
||||
private bool isLoad;
|
||||
|
||||
// UI 建立
|
||||
public MyCard() : base(BlurBorder.BackgroundProperty)
|
||||
{
|
||||
MainChrome = new MyDropShadow
|
||||
{
|
||||
Margin = new Thickness(-3, -3, -3, -3 - ModBase.GetWPFSize(1d)), ShadowRadius = 3d,
|
||||
Opacity = dropShadowIdleOpacity, CornerRadius = new CornerRadius(5d)
|
||||
};
|
||||
MainChrome.SetResourceReference(MyDropShadow.ColorProperty, "ColorObject1");
|
||||
Children.Insert(0, MainChrome);
|
||||
mainBorder = new BlurBorder { CornerRadius = new CornerRadius(5d), IsHitTestVisible = false };
|
||||
Children.Insert(1, mainBorder);
|
||||
mainGrid = new Grid();
|
||||
Children.Add(mainGrid);
|
||||
// 设置背景色
|
||||
SetResourceReference(BackgroundBrushProperty, "ColorBrushTransparentBackground");
|
||||
Loaded += (_, _) => Init();
|
||||
MouseEnter += MyCard_MouseEnter;
|
||||
MouseLeave += MyCard_MouseLeave;
|
||||
SizeChanged += MySizeChanged;
|
||||
MouseLeftButtonDown += MyCard_MouseLeftButtonDown;
|
||||
MouseLeftButtonUp += MyCard_MouseLeftButtonUp;
|
||||
MouseLeave += MyCard_MouseLeave_Swap;
|
||||
}
|
||||
|
||||
public MyDropShadow MainChrome { get; }
|
||||
|
||||
public UIElement BorderChild
|
||||
{
|
||||
get => mainBorder.Child;
|
||||
set => mainBorder.Child = value;
|
||||
}
|
||||
|
||||
public TextBlock MainTextBlock
|
||||
{
|
||||
get
|
||||
{
|
||||
Init(); // 当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。
|
||||
return _MainTextBlock;
|
||||
}
|
||||
set => _MainTextBlock = value;
|
||||
}
|
||||
|
||||
public Path MainSwap
|
||||
{
|
||||
get
|
||||
{
|
||||
Init();
|
||||
return field;
|
||||
}
|
||||
set => field = value;
|
||||
}
|
||||
|
||||
// 属性
|
||||
public InlineCollection Inlines => MainTextBlock.Inlines;
|
||||
|
||||
public CornerRadius CornerRadius
|
||||
{
|
||||
get => MainChrome.CornerRadius;
|
||||
set
|
||||
{
|
||||
MainChrome.CornerRadius = value;
|
||||
mainBorder.CornerRadius = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set
|
||||
{
|
||||
SetValue(TitleProperty, value);
|
||||
if (_MainTextBlock is not null)
|
||||
MainTextBlock.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override SolidColorBrush AnimatableBrush
|
||||
{
|
||||
get => (SolidColorBrush)mainBorder.Background;
|
||||
set => mainBorder.Background = value;
|
||||
}
|
||||
|
||||
protected override FrameworkElement AnimatableElement => mainBorder;
|
||||
public bool HasMouseAnimation { get; set; } = true;
|
||||
|
||||
private void Init()
|
||||
{
|
||||
if (isLoad)
|
||||
return;
|
||||
isLoad = true;
|
||||
// AddHandler ThemeChanged, AddressOf _BackgroundBrushChanged '已在依赖属性中实现
|
||||
// 初次加载限定
|
||||
if (MainTextBlock is null)
|
||||
{
|
||||
MainTextBlock = new TextBlock
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(15d, 12d, 0d, 0d), FontWeight = FontWeights.Bold, FontSize = 13d,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1");
|
||||
MainTextBlock.SetBinding(TextBlock.TextProperty,
|
||||
new Binding("Title") { Source = this, Mode = BindingMode.OneWay });
|
||||
mainGrid.Children.Add(MainTextBlock);
|
||||
}
|
||||
|
||||
if (CanSwap || SwapControl is not null)
|
||||
{
|
||||
if (SwapControl is null && Children.Count > 3)
|
||||
SwapControl = Children[3];
|
||||
MainSwap = new Path
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right, Stretch = Stretch.Uniform, Height = 6d, Width = 10d,
|
||||
VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0d, 17d, 16d, 0d),
|
||||
Data =
|
||||
(Geometry)new GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"),
|
||||
RenderTransform = new RotateTransform(180d), RenderTransformOrigin = new Point(0.5d, 0.5d)
|
||||
};
|
||||
MainSwap.SetResourceReference(Shape.FillProperty, "ColorBrush1");
|
||||
mainGrid.Children.Add(MainSwap);
|
||||
}
|
||||
|
||||
// 改变默认的折叠
|
||||
if (IsSwapped && SwapControl is not null)
|
||||
{
|
||||
MainSwap.RenderTransform = new RotateTransform(SwapLogoRight ? 270 : 0);
|
||||
SwapControl.Visibility = Visibility.Collapsed;
|
||||
// 取消由于高度变化被迫触发的高度动画
|
||||
var rawUseAnimation = UseAnimation;
|
||||
UseAnimation = false;
|
||||
Height = SwapedHeight;
|
||||
ModAnimation.AniStop("MyCard Height " + uuid);
|
||||
isHeightAnimating = false;
|
||||
ModBase.RunInUi(() => UseAnimation = rawUseAnimation, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void StackInstall()
|
||||
{
|
||||
var argstack = (StackPanel)SwapControl;
|
||||
StackInstall(ref argstack, InstallMethod);
|
||||
SwapControl = argstack;
|
||||
TriggerForceResize();
|
||||
}
|
||||
|
||||
public static void StackInstall(ref StackPanel stack, Action<StackPanel> installMethod)
|
||||
{
|
||||
if (stack.Tag is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
installMethod(stack);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[MyCard] InstallMethod 调用失败");
|
||||
}
|
||||
|
||||
stack.Children.Add(new FrameworkElement { Height = 18d }); // 下边距,同时适应折叠
|
||||
stack.Tag = null;
|
||||
}
|
||||
|
||||
private void MyCard_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!HasMouseAnimation)
|
||||
return;
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
if (MainTextBlock is not null)
|
||||
aniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 90));
|
||||
if (MainSwap is not null)
|
||||
aniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush2", 90));
|
||||
aniList.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject4", 90),
|
||||
ModAnimation.AaOpacity(MainChrome, dropShadowHoverOpacity - MainChrome.Opacity, 90)
|
||||
});
|
||||
if (!IsAnimating)
|
||||
ModAnimation.AniStart(aniList, "MyCard Mouse " + uuid);
|
||||
}
|
||||
|
||||
private void MyCard_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!HasMouseAnimation)
|
||||
return;
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
if (MainTextBlock is not null)
|
||||
aniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 90));
|
||||
if (MainSwap is not null)
|
||||
aniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush1", 90));
|
||||
aniList.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject1", 90),
|
||||
ModAnimation.AaOpacity(MainChrome, dropShadowIdleOpacity - MainChrome.Opacity, 90)
|
||||
});
|
||||
if (!IsAnimating)
|
||||
ModAnimation.AniStart(aniList, "MyCard Mouse " + uuid);
|
||||
}
|
||||
|
||||
#region 高度改变动画
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用高度改变动画。
|
||||
/// </summary>
|
||||
public bool UseAnimation { get; set; } = true;
|
||||
|
||||
private bool isHeightAnimating;
|
||||
private double actualUsedHeight; // 回滚实际高度(例如 NaN)
|
||||
|
||||
private void MySizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (!UseAnimation)
|
||||
return;
|
||||
var deltaHeight = (IsSwapped ? SwapedHeight : e.NewSize.Height) - e.PreviousSize.Height;
|
||||
// 卡片的进入时动画已被页面通用切换动画替代
|
||||
if (e.PreviousSize.Height == 0d || isHeightAnimating || Math.Abs(deltaHeight) < 1d || ActualHeight == 0d)
|
||||
return;
|
||||
StartHeightAnimation(deltaHeight, e.PreviousSize.Height, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动卡片高度变化的动画效果
|
||||
/// 根据变化距离的大小采用不同的动画策略:短距离使用简单缓动,长距离使用分段动画
|
||||
/// </summary>
|
||||
/// <param name="delta">高度变化量</param>
|
||||
/// <param name="previousHeight">之前的高度</param>
|
||||
/// <param name="isLoadAnimation">是否为加载动画</param>
|
||||
private void StartHeightAnimation(double delta, double previousHeight, bool isLoadAnimation)
|
||||
{
|
||||
if (isHeightAnimating || ModMain.frmMain is null)
|
||||
return; // 避免 XAML 设计器出错
|
||||
|
||||
var animList = new List<ModAnimation.AniData>();
|
||||
var absDelta = Math.Abs(delta);
|
||||
|
||||
if (absDelta <= 800d)
|
||||
{
|
||||
// 短距离,直接使用 150ms 的缓动动画
|
||||
animList.Add(ModAnimation.AaHeight(this, delta, 150,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var easeLength = default(int);
|
||||
int easeTime;
|
||||
int initSpeed; // 到达缓动区前的初速度
|
||||
if (delta < 0d && absDelta - easeLength > 5000d * 0.1d)
|
||||
{
|
||||
// 收回距离过长 (>0.1s),强制以 100ms 完成匀速段,然后让减速段更长
|
||||
easeLength = 200;
|
||||
easeTime = 150;
|
||||
initSpeed = (int)Math.Round((absDelta - easeLength) / 0.1d);
|
||||
}
|
||||
else if (delta > 0d && absDelta - easeLength > 5000d * 0.6d)
|
||||
{
|
||||
// 展开距离过长 (>0.6s),以 5000 速度展示 300ms 匀速段,剩下的距离全部归入减速段
|
||||
initSpeed = 5000;
|
||||
easeLength = (int)Math.Round(absDelta - initSpeed * 0.3d);
|
||||
easeTime = 400;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 中程,匀速地快速展开(或收回)
|
||||
easeLength = 150;
|
||||
easeTime = 200;
|
||||
initSpeed = 4000;
|
||||
}
|
||||
|
||||
// 匀速段
|
||||
animList.Add(ModAnimation.AaHeight(this, (absDelta - easeLength) * Math.Sign(delta),
|
||||
(int)Math.Round((absDelta - easeLength) / initSpeed * 1000d)));
|
||||
// 减速段
|
||||
animList.Add(ModAnimation.AaHeight(this, easeLength * Math.Sign(delta), easeTime,
|
||||
ease: new ModAnimation.AniEaseOutFluentWithInitial(initSpeed, easeTime / 1000d, easeLength),
|
||||
after: true));
|
||||
}
|
||||
|
||||
animList.Add(ModAnimation.AaCode(() =>
|
||||
{
|
||||
isHeightAnimating = false;
|
||||
Height = actualUsedHeight;
|
||||
if (IsSwapped && SwapControl is not null)
|
||||
SwapControl.Visibility = Visibility.Collapsed;
|
||||
}, after: true));
|
||||
ModAnimation.AniStart(animList, "MyCard Height " + uuid);
|
||||
isHeightAnimating = true;
|
||||
actualUsedHeight = IsSwapped ? SwapedHeight : Height;
|
||||
Height = previousHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。
|
||||
/// </summary>
|
||||
public void TriggerForceResize()
|
||||
{
|
||||
Height = IsSwapped ? SwapedHeight : double.NaN;
|
||||
ModAnimation.AniStop("MyCard Height " + uuid);
|
||||
isHeightAnimating = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 折叠
|
||||
|
||||
// 若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠
|
||||
// 这是因为不能直接在 XAML 中设置 SwapControl
|
||||
public UIElement SwapControl;
|
||||
public bool CanSwap { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 数据转为列表项的转换方法
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Action<StackPanel> InstallMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已被折叠。
|
||||
/// </summary>
|
||||
public bool IsSwapped
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
if (SwapControl is null)
|
||||
return;
|
||||
|
||||
// 当卡片展开时,如果SwapControl是StackPanel类型,则执行安装方法
|
||||
// 这通常用于动态添加内容到折叠卡片中
|
||||
if (!IsSwapped && SwapControl is StackPanel)
|
||||
{
|
||||
var argstack = (StackPanel)SwapControl;
|
||||
StackInstall(ref argstack, InstallMethod);
|
||||
SwapControl = argstack;
|
||||
}
|
||||
|
||||
// 若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
|
||||
// 更新控件的可见性和高度
|
||||
SwapControl.Visibility = Visibility.Visible;
|
||||
TriggerForceResize();
|
||||
|
||||
// 根据折叠状态旋转箭头图标
|
||||
// 折叠时箭头指向右侧或向上(根据SwapLogoRight设置),展开时指向下方
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaRotateTransform(MainSwap,
|
||||
(field ? SwapLogoRight ? 270 : 0 : 180) - ((RotateTransform)MainSwap.RenderTransform).Angle,
|
||||
250, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
"MyCard Swap " + uuid, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否已被折叠。(已过时,请使用 IsSwapped)
|
||||
/// </summary>
|
||||
[Obsolete("请使用 IsSwapped 属性,IsSwaped 存在拼写错误")]
|
||||
public bool IsSwaped
|
||||
{
|
||||
get => IsSwapped;
|
||||
set => IsSwapped = value;
|
||||
}
|
||||
|
||||
public bool SwapLogoRight { get; set; } = false;
|
||||
private bool isSwapMouseDown = false; //用于触发卡片展开/折叠的 MouseDown
|
||||
private bool isCustomMouseDown = false; //用于触发自定义事件的 MouseDown
|
||||
public event PreviewSwapEventHandler? PreviewSwap;
|
||||
|
||||
public delegate void PreviewSwapEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
public event SwapEventHandler? Swap;
|
||||
|
||||
public delegate void SwapEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
public const int SwapedHeight = 40;
|
||||
|
||||
private void MyCard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
double pos = Mouse.GetPosition(this).Y;
|
||||
if (!IsSwapped && (pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver)))
|
||||
return;
|
||||
isCustomMouseDown = true;
|
||||
if (!IsSwapped && (SwapControl is null || pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver)))
|
||||
return;
|
||||
isSwapMouseDown = true;
|
||||
}
|
||||
|
||||
private void MyCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isCustomMouseDown) return;
|
||||
isCustomMouseDown = false;
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
|
||||
if (!isSwapMouseDown) return;
|
||||
isSwapMouseDown = false;
|
||||
|
||||
double pos = Mouse.GetPosition(this).Y;
|
||||
if (!IsSwapped && (SwapControl is null || pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver)))
|
||||
return; // 检测点击位置;或已经不在可视树上的误判
|
||||
|
||||
var e2 = new ModBase.RouteEventArgs(true);
|
||||
PreviewSwap?.Invoke(this, e2);
|
||||
if (e2.handled)
|
||||
{
|
||||
isSwapMouseDown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
IsSwapped = !IsSwapped;
|
||||
ModBase.Log("[Control] " + (IsSwapped ? "折叠卡片" : "展开卡片") + (Title is null ? "" : ":" + Title));
|
||||
Swap?.Invoke(this, e2);
|
||||
}
|
||||
|
||||
private void MyCard_MouseLeave_Swap(object sender, MouseEventArgs e)
|
||||
{
|
||||
isSwapMouseDown = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static partial class ModAnimation
|
||||
{
|
||||
public static void AniDispose(MyCard control, bool removeFromChildren, ParameterizedThreadStart callBack = null)
|
||||
{
|
||||
if (control.IsHitTestVisible)
|
||||
{
|
||||
control.IsHitTestVisible = false;
|
||||
AniStart(new[]
|
||||
{
|
||||
AaScaleTransform(control, -0.08d, 200, ease: new AniEaseInFluent()),
|
||||
AaOpacity(control, -1, 200, ease: new AniEaseOutFluent()),
|
||||
AaHeight(control, -control.ActualHeight, 150, 100, new AniEaseOutFluent()),
|
||||
AaCode(() =>
|
||||
{
|
||||
if (removeFromChildren)
|
||||
{
|
||||
if (control.Parent is null)
|
||||
return;
|
||||
((Panel)control.Parent).Children.Remove(control);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (callBack is not null)
|
||||
callBack(control);
|
||||
}, after: true)
|
||||
}, "MyCard Dispose " + control.uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (removeFromChildren)
|
||||
{
|
||||
if (control.Parent is null)
|
||||
return;
|
||||
((Panel)control.Parent).Children.Remove(control);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (callBack is not null)
|
||||
callBack(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Grid
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="PCL.MyCheckBox"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True" d:DesignWidth="126.4"
|
||||
d:DesignHeight="44.8">
|
||||
<TextBlock IsHitTestVisible="False" FontSize="13" x:FieldModifier="public" x:Name="LabText" Padding="0"
|
||||
Margin="26,0,0,0" Foreground="{DynamicResource ColorBrush1}" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" />
|
||||
<Border IsHitTestVisible="False" x:Name="ShapeBorder" BorderThickness="1.1" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" Width="18" Height="18" BorderBrush="{DynamicResource ColorBrush1}"
|
||||
CornerRadius="3" Margin="1,0,0,0" Background="{DynamicResource ColorBrushHalfWhite}" />
|
||||
<Path IsHitTestVisible="False" x:Name="ShapeCheck" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Fill="{Binding BorderBrush, ElementName=ShapeBorder}" Width="12" Height="12" Margin="4,0,0,0"
|
||||
Data="M0,6L1.5,4.5 4.5,7.5 10.5,1.5 12,3 4.5,10.5 0,6z">
|
||||
<Path.RenderTransform>
|
||||
<ScaleTransform CenterX="6" CenterY="6" ScaleX="0" ScaleY="0" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Border IsHitTestVisible="False" x:Name="ShapeIndeterminate" BorderThickness="0" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" Width="10" Height="10" Margin="5,0,0,0" CornerRadius="2"
|
||||
Background="{Binding BorderBrush, ElementName=ShapeBorder}">
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform CenterX="6" CenterY="6" ScaleX="0" ScaleY="0" />
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,429 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyCheckBox
|
||||
{
|
||||
public delegate void ChangeEventHandler(object sender, bool user);
|
||||
|
||||
public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
private const int animationTimeOfCheck = 150; // 勾选状态变更动画长度
|
||||
|
||||
// 指向动画
|
||||
|
||||
private const int animationTimeOfMouseIn = 100;
|
||||
|
||||
private const int animationTimeOfMouseOut = 200;
|
||||
|
||||
// 在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变
|
||||
public static readonly DependencyProperty CheckedProperty = DependencyProperty.Register("Checked", typeof(bool?),
|
||||
typeof(MyCheckBox), new PropertyMetadata(false, (d, e) =>
|
||||
{
|
||||
var obj = (MyCheckBox)d;
|
||||
if (!obj.IsLoaded) obj.SyncUI();
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// 是否为三态复选框。
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsThreeStateProperty =
|
||||
DependencyProperty.Register("IsThreeState", typeof(bool), typeof(MyCheckBox), new PropertyMetadata(false));
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyCheckBox), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyCheckBox)sender).LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
private bool? _previousState = false; // 上一次的勾选状态
|
||||
private bool allowMouseDown = true;
|
||||
|
||||
// 点击事件
|
||||
|
||||
private bool mouseDowned;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyCheckBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MouseLeftButtonUp += (_, _) => Checkbox_MouseUp();
|
||||
MouseLeftButtonDown += (_, _) => Checkbox_MouseDown();
|
||||
MouseLeave += (_, _) => Checkbox_MouseLeave();
|
||||
IsEnabledChanged += (_, _) => Checkbox_IsEnabledChanged();
|
||||
MouseEnter += (_, _) => Checkbox_MouseEnterAnimation();
|
||||
MouseLeave += (_, _) => Checkbox_MouseLeaveAnimation();
|
||||
}
|
||||
|
||||
// 自定义属性
|
||||
public bool? Checked
|
||||
{
|
||||
get => (bool?)GetValue(CheckedProperty);
|
||||
set => SetChecked(value, false);
|
||||
}
|
||||
|
||||
public bool IsThreeState
|
||||
{
|
||||
get => (bool)GetValue(IsThreeStateProperty);
|
||||
set => SetValue(IsThreeStateProperty, value);
|
||||
} // 是否为三态复选框
|
||||
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
} // 内容
|
||||
|
||||
/// <summary>
|
||||
/// 复选框勾选状态改变。
|
||||
/// </summary>
|
||||
/// <param name="user">是否为用户手动改变的勾选状态。</param>
|
||||
public event ChangeEventHandler? Change;
|
||||
|
||||
public event PreviewChangeEventHandler? PreviewChange;
|
||||
|
||||
/// <summary>
|
||||
/// 手动设置 Checked 属性。
|
||||
/// </summary>
|
||||
/// <param name="value">新的 Checked 属性。</param>
|
||||
/// <param name="user">是否由用户引发。</param>
|
||||
public void SetChecked(bool? value, bool user)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Checked.HasValue && value.HasValue && Checked.Value == value.Value)
|
||||
return;
|
||||
|
||||
// Preview 事件
|
||||
if (value.HasValue && value.Value && user)
|
||||
{
|
||||
var e = new ModBase.RouteEventArgs(user);
|
||||
PreviewChange?.Invoke(this, e);
|
||||
if (e.handled)
|
||||
{
|
||||
mouseDowned = true;
|
||||
Checkbox_MouseLeave();
|
||||
mouseDowned = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断真实勾选状态
|
||||
var isChecked = GetFinalState(value, IsThreeState);
|
||||
|
||||
_previousState = Checked; // 记录上一次的勾选状态
|
||||
SetValue(CheckedProperty, isChecked);
|
||||
if (IsLoaded)
|
||||
Change?.Invoke(this, user);
|
||||
|
||||
// 更改动画
|
||||
SyncUI();
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "设置 Checked 失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncUI()
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
allowMouseDown = false;
|
||||
|
||||
var isChecked = GetFinalState(Checked, IsThreeState);
|
||||
|
||||
switch (isChecked, _previousState)
|
||||
{
|
||||
case (true, null):
|
||||
AniBackgroundScale();
|
||||
AniIndeterminateHide();
|
||||
AniCheckShow();
|
||||
AniColorChecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
|
||||
case (true, false):
|
||||
AniBackgroundScale();
|
||||
AniCheckShow();
|
||||
AniColorChecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
|
||||
case (false, true):
|
||||
AniBackgroundScale();
|
||||
AniCheckHide();
|
||||
AniColorUnchecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
|
||||
case (false, null):
|
||||
AniBackgroundScale();
|
||||
AniIndeterminateHide();
|
||||
AniCheckHide();
|
||||
AniColorUnchecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
|
||||
case (null, true):
|
||||
AniBackgroundScale();
|
||||
AniCheckHide();
|
||||
AniIndeterminateShow();
|
||||
AniColorUnchecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
|
||||
case (null, false):
|
||||
AniBackgroundScale();
|
||||
AniIndeterminateShow();
|
||||
AniColorUnchecked();
|
||||
AniAllowMouseDown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不使用动画
|
||||
ModAnimation.AniStop("MyCheckBox Background Scale " + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox Check Scale Show" + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox Check Scale Hide" + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox Indeterminate Scale Show" + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox Indeterminate Scale Hide" + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox AllowMouseDown " + Uuid);
|
||||
if (Checked == true)
|
||||
{
|
||||
((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 1d;
|
||||
((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 1d;
|
||||
ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
|
||||
IsEnabled ? "ColorBrush2" : "ColorBrushGray4");
|
||||
}
|
||||
else
|
||||
{
|
||||
((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 0d;
|
||||
((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 0d;
|
||||
ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
|
||||
IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Checkbox_MouseUp()
|
||||
{
|
||||
if (!mouseDowned)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下复选框(" + !Checked + "):" + Text);
|
||||
mouseDowned = false;
|
||||
if (IsThreeState)
|
||||
{
|
||||
switch (Checked)
|
||||
{
|
||||
case true:
|
||||
SetChecked(null, true);
|
||||
break;
|
||||
case false:
|
||||
SetChecked(true, true);
|
||||
break;
|
||||
case null:
|
||||
SetChecked(false, true);
|
||||
break;
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
|
||||
"MyCheckBox Background " + Uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
SetChecked(!Checked, true);
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
|
||||
"MyCheckBox Background " + Uuid);
|
||||
}
|
||||
|
||||
private void Checkbox_MouseDown()
|
||||
{
|
||||
if (!allowMouseDown)
|
||||
return;
|
||||
mouseDowned = true;
|
||||
Focus();
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushBg1", 100),
|
||||
"MyCheckBox Background " + Uuid);
|
||||
var scaleAnims = new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), absolute: true)
|
||||
};
|
||||
if (Checked == true)
|
||||
scaleAnims.Add(ModAnimation.AaScaleTransform(ShapeCheck,
|
||||
0.9d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, 1000,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)));
|
||||
ModAnimation.AniStart(scaleAnims.ToArray(), "MyCheckBox Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void Checkbox_MouseLeave()
|
||||
{
|
||||
if (!mouseDowned)
|
||||
return;
|
||||
mouseDowned = false;
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
|
||||
"MyCheckBox Background " + Uuid);
|
||||
var scaleAnims = new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), absolute: true)
|
||||
};
|
||||
if (Checked == true)
|
||||
scaleAnims.Add(ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
|
||||
500, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)));
|
||||
ModAnimation.AniStart(scaleAnims.ToArray(), "MyCheckBox Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void Checkbox_IsEnabledChanged()
|
||||
{
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
if (IsEnabled)
|
||||
{
|
||||
// 可用
|
||||
Checkbox_MouseLeaveAnimation();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不可用
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
|
||||
ThemeManager.colorGray4 - ShapeBorder.BorderBrush, animationTimeOfMouseOut),
|
||||
"MyCheckBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
ThemeManager.colorGray4 - LabText.Foreground, animationTimeOfMouseOut),
|
||||
"MyCheckBox TextColor " + Uuid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyCheckBox TextColor " + Uuid);
|
||||
ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid);
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
|
||||
ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
|
||||
IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4");
|
||||
}
|
||||
}
|
||||
|
||||
private void Checkbox_MouseEnterAnimation()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", animationTimeOfMouseIn),
|
||||
"MyCheckBox TextColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, "ColorBrush3", animationTimeOfMouseIn),
|
||||
"MyCheckBox BorderColor " + Uuid);
|
||||
}
|
||||
|
||||
private void Checkbox_MouseLeaveAnimation()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return; // MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
IsEnabled ? "ColorBrush1" : "ColorBrushGray4", animationTimeOfMouseOut),
|
||||
"MyCheckBox TextColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
|
||||
IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4",
|
||||
animationTimeOfMouseOut),
|
||||
"MyCheckBox BorderColor " + Uuid);
|
||||
}
|
||||
|
||||
// 动画
|
||||
private void AniBackgroundScale()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScale(ShapeBorder, 12d - ShapeBorder.Width, animationTimeOfCheck,
|
||||
ease: new ModAnimation.AniEaseOutFluent(), absolute: true),
|
||||
ModAnimation.AaScale(ShapeBorder, 6d, animationTimeOfCheck * 2,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.7d), new ModAnimation.AniEaseOutBack(), absolute: true)
|
||||
}, "MyCheckBox Background Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void AniCheckShow()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
|
||||
animationTimeOfCheck * 2, (int)Math.Round(animationTimeOfCheck * 0.7d),
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
"MyCheckBox Check Scale Show" + Uuid);
|
||||
}
|
||||
|
||||
private void AniCheckHide()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeCheck, -((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.9d),
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
"MyCheckBox Check Scale Hide" + Uuid);
|
||||
}
|
||||
|
||||
private void AniIndeterminateShow()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeIndeterminate,
|
||||
1d - ((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX, animationTimeOfCheck * 2,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.7d),
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
"MyCheckBox Indeterminate Scale Show" + Uuid);
|
||||
}
|
||||
|
||||
private void AniIndeterminateHide()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeIndeterminate,
|
||||
-((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.9d),
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
"MyCheckBox Indeterminate Scale Hide" + Uuid);
|
||||
}
|
||||
|
||||
private void AniAllowMouseDown()
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaCode(() => allowMouseDown = true, animationTimeOfCheck * 2),
|
||||
"MyCheckBox AllowMouseDown " + Uuid);
|
||||
}
|
||||
|
||||
private void AniColorChecked()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
|
||||
IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush2" : "ColorBrushGray4", animationTimeOfCheck),
|
||||
"MyCheckBox BorderColor " + Uuid);
|
||||
}
|
||||
|
||||
private void AniColorUnchecked()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
|
||||
IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush1" : "ColorBrushGray4", animationTimeOfCheck),
|
||||
"MyCheckBox BorderColor " + Uuid);
|
||||
}
|
||||
|
||||
private static bool? GetFinalState(bool? value, bool isThreeState)
|
||||
{
|
||||
return isThreeState ? value : value == true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
/// <summary>
|
||||
/// 轻量折叠栏:一行可点击标题 + 三角,点击切换其下内容区的显示。
|
||||
/// 无卡片外观(无阴影/边框/背景),带高度折叠动画。
|
||||
/// </summary>
|
||||
public class MyCollapseBar : StackPanel
|
||||
{
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(MyCollapseBar),
|
||||
new PropertyMetadata("", (d, e) => ((MyCollapseBar)d)._titleBlock.Text = (string)e.NewValue));
|
||||
|
||||
private const double HeaderHeight = 30d;
|
||||
|
||||
private readonly int _uuid = ModBase.GetUuid();
|
||||
private readonly TextBlock _titleBlock;
|
||||
private readonly Path _triangle;
|
||||
private readonly StackPanel _contentPanel;
|
||||
private (MyCard card, bool useAnimation)? _parentCardState;
|
||||
|
||||
public MyCollapseBar()
|
||||
{
|
||||
Orientation = Orientation.Vertical;
|
||||
ClipToBounds = true;
|
||||
|
||||
_titleBlock = new TextBlock
|
||||
{
|
||||
FontSize = 14d, FontWeight = FontWeights.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Margin = new Thickness(6d, 0d, 0d, 0d), IsHitTestVisible = false
|
||||
};
|
||||
_titleBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1");
|
||||
|
||||
_triangle = new Path
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
||||
Stretch = Stretch.Uniform, Height = 6d, Width = 10d, Margin = new Thickness(0d, 0d, 12d, 0d),
|
||||
IsHitTestVisible = false,
|
||||
Data = (Geometry)new GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"),
|
||||
RenderTransform = new RotateTransform(180d), RenderTransformOrigin = new Point(0.5d, 0.5d)
|
||||
};
|
||||
_triangle.SetResourceReference(Shape.FillProperty, "ColorBrush1");
|
||||
|
||||
var header = new Grid { Height = HeaderHeight, Background = Brushes.Transparent, Cursor = Cursors.Hand };
|
||||
header.Children.Add(_titleBlock);
|
||||
header.Children.Add(_triangle);
|
||||
header.MouseLeftButtonUp += (_, _) => IsCollapsed = !IsCollapsed;
|
||||
|
||||
_contentPanel = new StackPanel { Margin = new Thickness(6d, 2d, 0d, 0d) };
|
||||
|
||||
Children.Add(header);
|
||||
Children.Add(_contentPanel);
|
||||
}
|
||||
|
||||
/// <summary>标题文本。</summary>
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>内容区,调用方向其 Children 添加要折叠的内容。</summary>
|
||||
public StackPanel ContentPanel => _contentPanel;
|
||||
|
||||
/// <summary>开合状态实际改变时触发。</summary>
|
||||
public event EventHandler? Toggled;
|
||||
|
||||
/// <summary>是否收起。</summary>
|
||||
public bool IsCollapsed
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value) return;
|
||||
field = value;
|
||||
|
||||
var target = value ? 0d : 180d;
|
||||
if (IsLoaded)
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaRotateTransform(_triangle,
|
||||
target - ((RotateTransform)_triangle.RenderTransform).Angle, 250,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
"MyCollapseBar " + _uuid, true);
|
||||
else
|
||||
((RotateTransform)_triangle.RenderTransform).Angle = target;
|
||||
|
||||
if (IsLoaded && ActualHeight > 0)
|
||||
{
|
||||
if (value)
|
||||
CollapseWithAnimation();
|
||||
else
|
||||
ExpandWithAnimation();
|
||||
}
|
||||
else
|
||||
{
|
||||
_contentPanel.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
Toggled?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void CollapseWithAnimation()
|
||||
{
|
||||
ModAnimation.AniStop("MyCollapseBar Height " + _uuid);
|
||||
RestoreParentCardOnInterrupt();
|
||||
SilenceParentCard();
|
||||
|
||||
var fullHeight = ActualHeight;
|
||||
Height = fullHeight;
|
||||
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaHeight(this, HeaderHeight - fullHeight, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
_contentPanel.Visibility = Visibility.Collapsed;
|
||||
Height = double.NaN;
|
||||
RestoreParentCard();
|
||||
}, after: true)
|
||||
}, "MyCollapseBar Height " + _uuid);
|
||||
}
|
||||
|
||||
private void ExpandWithAnimation()
|
||||
{
|
||||
ModAnimation.AniStop("MyCollapseBar Height " + _uuid);
|
||||
RestoreParentCardOnInterrupt();
|
||||
SilenceParentCard();
|
||||
|
||||
_contentPanel.Visibility = Visibility.Visible;
|
||||
Height = double.NaN;
|
||||
Measure(new Size(ActualWidth, double.PositiveInfinity));
|
||||
var fullHeight = DesiredSize.Height;
|
||||
Height = HeaderHeight;
|
||||
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaHeight(this, fullHeight - HeaderHeight, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
Height = double.NaN;
|
||||
RestoreParentCard();
|
||||
}, after: true)
|
||||
}, "MyCollapseBar Height " + _uuid);
|
||||
}
|
||||
|
||||
/// <summary>若上一个动画被中断且它的 RestoreParentCard 未执行,则先恢复。</summary>
|
||||
private void RestoreParentCardOnInterrupt()
|
||||
{
|
||||
if (_parentCardState is { } s)
|
||||
{
|
||||
s.card.UseAnimation = s.useAnimation;
|
||||
_parentCardState = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SilenceParentCard()
|
||||
{
|
||||
if (_parentCardState is not null)
|
||||
return;
|
||||
|
||||
var current = Parent as FrameworkElement;
|
||||
while (current is not null)
|
||||
{
|
||||
if (current is MyCard card)
|
||||
{
|
||||
_parentCardState = (card, card.UseAnimation);
|
||||
card.UseAnimation = false;
|
||||
return;
|
||||
}
|
||||
current = current.Parent as FrameworkElement;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreParentCard()
|
||||
{
|
||||
if (_parentCardState is { } s)
|
||||
{
|
||||
s.card.UseAnimation = s.useAnimation;
|
||||
_parentCardState = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public class MyComboBox : ComboBox
|
||||
{
|
||||
public delegate void TextChangedEventHandler(object sender, TextChangedEventArgs e);
|
||||
|
||||
public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string),
|
||||
typeof(MyComboBox), new PropertyMetadata("", (d, e) =>
|
||||
{
|
||||
var c = (MyComboBox)d;
|
||||
if (c.textBox is not null)
|
||||
c.textBox.HintText = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
private string _Text;
|
||||
|
||||
// 鼠标按下接口
|
||||
private bool isMouseDown;
|
||||
|
||||
// 修复 WPF Bug:下拉框文本修改后,依然误认为还选择着此前的选项,导致再次点击该选项时内容不变
|
||||
private bool isTextChanging;
|
||||
private double realWidth; // 由于下拉框 Popup 宽度与 Width 一致,故不能为 NaN(Auto)
|
||||
private MyTextBox textBox;
|
||||
|
||||
// 基础
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyComboBox()
|
||||
{
|
||||
_Text = SelectedItem?.ToString() ?? "";
|
||||
PreviewMouseLeftButtonDown += MyComboBox_PreviewMouseLeftButtonDown;
|
||||
PreviewMouseLeftButtonUp += MyComboBox_PreviewMouseLeftButtonUp;
|
||||
MouseLeave += MyComboBox_PreviewMouseLeftButtonUp;
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
PreviewMouseLeftButtonDown += (_, _) => RefreshColor();
|
||||
PreviewMouseLeftButtonUp += (_, _) => RefreshColor();
|
||||
GotKeyboardFocus += (_, _) => RefreshColor();
|
||||
DropDownOpened += MyComboBox_DropDownOpened;
|
||||
DropDownClosed += MyComboBox_DropDownClosed;
|
||||
TextChanged += MyComboBox_TextChanged;
|
||||
}
|
||||
|
||||
public string HintText
|
||||
{
|
||||
get => (string)GetValue(HintTextProperty);
|
||||
set => SetValue(HintTextProperty, value);
|
||||
}
|
||||
|
||||
public new string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsEditable)
|
||||
{
|
||||
if (textBox is null) return _Text ?? "";
|
||||
return textBox.Text ?? "";
|
||||
}
|
||||
|
||||
return (SelectedItem ?? "").ToString();
|
||||
}
|
||||
set
|
||||
{
|
||||
if (IsEditable)
|
||||
{
|
||||
if (textBox is null)
|
||||
_Text = value;
|
||||
else
|
||||
textBox.Text = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("该 ComboBox 不支持修改文本。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool DropDownWidthSync { get; set; } = true;
|
||||
|
||||
public ContentPresenter ContentPresenter => (ContentPresenter)Template.FindName("PART_Content", this);
|
||||
public event TextChangedEventHandler? TextChanged;
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
if (!IsEditable)
|
||||
return;
|
||||
try
|
||||
{
|
||||
textBox = (MyTextBox)Template.FindName("PART_EditableTextBox", this);
|
||||
textBox.AddHandler(LostFocusEvent, new RoutedEventHandler((_, _) => RefreshColor()));
|
||||
textBox.changedEventList.Add((sender, e) => TextChanged?.Invoke(sender, (TextChangedEventArgs)e));
|
||||
textBox.Tag = Tag; // 有时需要用文本框的 Tag 来写入设置
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
textBox.Text = _Text;
|
||||
else
|
||||
TextChanged?.Invoke(this, null);
|
||||
if (HintText.Length > 0)
|
||||
textBox.HintText = HintText;
|
||||
textBox.SetResourceReference(TextBoxBase.CaretBrushProperty, "ColorBrushGray1");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"初始化可编辑文本框失败(" + (Name ?? "") + ")",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void MyComboBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isMouseDown = true;
|
||||
}
|
||||
|
||||
private void MyComboBox_PreviewMouseLeftButtonUp(object sender, EventArgs e)
|
||||
{
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
// 指向动画
|
||||
public void RefreshColor()
|
||||
{
|
||||
// 判断当前颜色
|
||||
string foreColorName;
|
||||
string backColorName;
|
||||
int time;
|
||||
if (IsEnabled)
|
||||
{
|
||||
if (isMouseDown || IsDropDownOpen ||
|
||||
(IsEditable && ((MyTextBox)Template.FindName("PART_EditableTextBox", this)).IsFocused))
|
||||
{
|
||||
foreColorName = "ColorBrush3";
|
||||
backColorName = "ColorBrush7";
|
||||
time = 10;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
foreColorName = "ColorBrush4";
|
||||
backColorName = "ColorBrush7";
|
||||
time = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreColorName = "ColorBrushBg0";
|
||||
backColorName = "ColorBrushHalfWhite";
|
||||
time = 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreColorName = "ColorBrushGray5";
|
||||
backColorName = "ColorBrushGray6";
|
||||
time = 200;
|
||||
}
|
||||
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, ForegroundProperty, foreColorName, time),
|
||||
ModAnimation.AaColor(this, BackgroundProperty, backColorName, time)
|
||||
}, "MyComboBox Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyComboBox Color " + Uuid);
|
||||
SetResourceReference(ForegroundProperty, foreColorName);
|
||||
SetResourceReference(BackgroundProperty, backColorName);
|
||||
}
|
||||
}
|
||||
|
||||
private void MyComboBox_DropDownOpened(object sender, EventArgs e)
|
||||
{
|
||||
realWidth = Width;
|
||||
if (DropDownWidthSync)
|
||||
Width = ActualWidth;
|
||||
try
|
||||
{
|
||||
var popup = (Grid)Template.FindName("PanPopup", this);
|
||||
popup.Opacity = ModMain.frmMain.Opacity;
|
||||
if (!DropDownWidthSync)
|
||||
popup.MinWidth = ActualWidth;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"设置下拉框属性失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void MyComboBox_DropDownClosed(object sender, EventArgs e)
|
||||
{
|
||||
Width = realWidth;
|
||||
}
|
||||
|
||||
private void MyComboBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (isTextChanging || !IsEditable)
|
||||
return;
|
||||
if (SelectedItem is null || Text == SelectedItem.ToString()) return;
|
||||
{
|
||||
var rawText = Text;
|
||||
var rawSelectionStart = textBox.SelectionStart;
|
||||
isTextChanging = true;
|
||||
SelectedItem = null;
|
||||
Text = rawText;
|
||||
textBox.SelectionStart = rawSelectionStart;
|
||||
isTextChanging = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于 ItemsSource 的自定义容器
|
||||
protected override DependencyObject GetContainerForItemOverride()
|
||||
{
|
||||
return new MyComboBoxItem();
|
||||
}
|
||||
|
||||
private void MyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
protected override bool IsItemItsOwnContainerOverride(object item)
|
||||
{
|
||||
return item is MyComboBoxItem || base.IsItemItsOwnContainerOverride(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyComboBoxItem : ComboBoxItem
|
||||
{
|
||||
// 指向动画
|
||||
|
||||
private const int animationTimeIn = 100;
|
||||
private const int animationTimeOut = 300;
|
||||
private string backColorName;
|
||||
private double fontOpacity;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyComboBoxItem()
|
||||
{
|
||||
Style = (Style)FindResource("MyComboBoxItem");
|
||||
Unselected += (_, _) => RefreshColor();
|
||||
MouseMove += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
Selected += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseLeftButtonUp += MyComboBoxItem_MouseLeftButtonUp;
|
||||
}
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
// 判断当前颜色
|
||||
string newBackColorName;
|
||||
double newFontOpacity;
|
||||
int time;
|
||||
if (IsSelected)
|
||||
{
|
||||
newBackColorName = "ColorBrush6";
|
||||
newFontOpacity = 1d;
|
||||
time = animationTimeIn;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
newBackColorName = "ColorBrush8";
|
||||
newFontOpacity = 1d;
|
||||
time = animationTimeIn;
|
||||
}
|
||||
else if (IsEnabled)
|
||||
{
|
||||
newBackColorName = "ColorBrushTransparent";
|
||||
newFontOpacity = 1d;
|
||||
time = animationTimeOut;
|
||||
}
|
||||
else
|
||||
{
|
||||
newBackColorName = "ColorBrushTransparent";
|
||||
newFontOpacity = 0.4d;
|
||||
time = animationTimeOut;
|
||||
}
|
||||
|
||||
if ((backColorName ?? "") == (newBackColorName ?? "") && fontOpacity == newFontOpacity)
|
||||
return;
|
||||
backColorName = newBackColorName;
|
||||
fontOpacity = newFontOpacity;
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, BackgroundProperty, backColorName, time),
|
||||
ModAnimation.AaOpacity(this, fontOpacity - Opacity, time)
|
||||
}, "ComboBoxItem Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("ComboBoxItem Color " + Uuid);
|
||||
SetResourceReference(BackgroundProperty, backColorName);
|
||||
Opacity = fontOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Content?.ToString() ?? "";
|
||||
}
|
||||
|
||||
public static implicit operator string(MyComboBoxItem value)
|
||||
{
|
||||
return value.Content?.ToString() ?? "";
|
||||
}
|
||||
|
||||
private void MyComboBoxItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ModBase.Log("[Control] 选择下拉列表项:" + ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyDropShadow : Decorator
|
||||
{
|
||||
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color),
|
||||
typeof(MyDropShadow),
|
||||
new FrameworkPropertyMetadata(Color.FromArgb(0x71, 0x0, 0x0, 0x0),
|
||||
FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));
|
||||
|
||||
public static readonly DependencyProperty ShadowRadiusProperty = DependencyProperty.Register("ShadowRadius",
|
||||
typeof(double), typeof(MyDropShadow),
|
||||
new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));
|
||||
|
||||
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius",
|
||||
typeof(CornerRadius), typeof(MyDropShadow),
|
||||
new FrameworkPropertyMetadata(new CornerRadius(), FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes),
|
||||
IsCornerRadiusValid);
|
||||
|
||||
private static Brush[] _commonBrushes;
|
||||
private static CornerRadius _commonCornerRadius;
|
||||
private static readonly object _resourceAccess = new();
|
||||
private Brush[] _brushes;
|
||||
|
||||
/// <summary>
|
||||
/// 阴影颜色。
|
||||
/// </summary>
|
||||
public Color Color
|
||||
{
|
||||
get => (Color)GetValue(ColorProperty);
|
||||
set => SetValue(ColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阴影模糊半径。
|
||||
/// </summary>
|
||||
public double ShadowRadius
|
||||
{
|
||||
get => (double)GetValue(ShadowRadiusProperty);
|
||||
set => SetValue(ShadowRadiusProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 圆角大小。
|
||||
/// </summary>
|
||||
public CornerRadius CornerRadius
|
||||
{
|
||||
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
private static bool IsCornerRadiusValid(object value)
|
||||
{
|
||||
var cr = (CornerRadius)value;
|
||||
return !(cr.TopLeft < 0.0d || cr.TopRight < 0.0d || cr.BottomLeft < 0.0d || cr.BottomRight < 0.0d ||
|
||||
double.IsNaN(cr.TopLeft) || double.IsNaN(cr.TopRight) || double.IsNaN(cr.BottomLeft) ||
|
||||
double.IsNaN(cr.BottomRight) || double.IsInfinity(cr.TopLeft) || double.IsInfinity(cr.TopRight) ||
|
||||
double.IsInfinity(cr.BottomLeft) || double.IsInfinity(cr.BottomRight));
|
||||
}
|
||||
|
||||
|
||||
// =======================================
|
||||
// 渲染
|
||||
// =======================================
|
||||
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext)
|
||||
{
|
||||
var cornerRadius = CornerRadius;
|
||||
var shadowBounds = new Rect(0d, 0d, RenderSize.Width, RenderSize.Height);
|
||||
var color = Color;
|
||||
|
||||
if (shadowBounds.Width > 0d && shadowBounds.Height > 0d && color.A > 0)
|
||||
{
|
||||
var centerWidth = shadowBounds.Right - shadowBounds.Left - 2d * ShadowRadius;
|
||||
var centerHeight = shadowBounds.Bottom - shadowBounds.Top - 2d * ShadowRadius;
|
||||
var maxRadius = Math.Min(centerWidth * 0.5d, centerHeight * 0.5d);
|
||||
cornerRadius.TopLeft = Math.Min(cornerRadius.TopLeft, maxRadius);
|
||||
cornerRadius.TopRight = Math.Min(cornerRadius.TopRight, maxRadius);
|
||||
cornerRadius.BottomLeft = Math.Min(cornerRadius.BottomLeft, maxRadius);
|
||||
cornerRadius.BottomRight = Math.Min(cornerRadius.BottomRight, maxRadius);
|
||||
var brushes = GetBrushes(color, cornerRadius);
|
||||
var centerTop = shadowBounds.Top + ShadowRadius;
|
||||
var centerLeft = shadowBounds.Left + ShadowRadius;
|
||||
var centerRight = shadowBounds.Right - ShadowRadius;
|
||||
var centerBottom = shadowBounds.Bottom - ShadowRadius;
|
||||
var guidelineSetX = new[]
|
||||
{
|
||||
centerLeft, centerLeft + cornerRadius.TopLeft, centerRight - cornerRadius.TopRight,
|
||||
centerLeft + cornerRadius.BottomLeft, centerRight - cornerRadius.BottomRight, centerRight
|
||||
};
|
||||
var guidelineSetY = new[]
|
||||
{
|
||||
centerTop, centerTop + cornerRadius.TopLeft, centerTop + cornerRadius.TopRight,
|
||||
centerBottom - cornerRadius.BottomLeft, centerBottom - cornerRadius.BottomRight, centerBottom
|
||||
};
|
||||
drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY));
|
||||
cornerRadius.TopLeft += ShadowRadius;
|
||||
cornerRadius.TopRight += ShadowRadius;
|
||||
cornerRadius.BottomLeft += ShadowRadius;
|
||||
cornerRadius.BottomRight += ShadowRadius;
|
||||
var topLeft = new Rect(shadowBounds.Left, shadowBounds.Top, cornerRadius.TopLeft, cornerRadius.TopLeft);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.TopLeft], null, topLeft);
|
||||
var topWidth = guidelineSetX[2] - guidelineSetX[1];
|
||||
|
||||
if (topWidth > 0d)
|
||||
{
|
||||
var top = new Rect(guidelineSetX[1], shadowBounds.Top, topWidth, ShadowRadius);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.Top], null, top);
|
||||
}
|
||||
|
||||
var topRight = new Rect(guidelineSetX[2], shadowBounds.Top, cornerRadius.TopRight, cornerRadius.TopRight);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.TopRight], null, topRight);
|
||||
var leftHeight = guidelineSetY[3] - guidelineSetY[1];
|
||||
|
||||
if (leftHeight > 0d)
|
||||
{
|
||||
var left = new Rect(shadowBounds.Left, guidelineSetY[1], ShadowRadius, leftHeight);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.Left], null, left);
|
||||
}
|
||||
|
||||
var rightHeight = guidelineSetY[4] - guidelineSetY[2];
|
||||
|
||||
if (rightHeight > 0d)
|
||||
{
|
||||
var right = new Rect(guidelineSetX[5], guidelineSetY[2], ShadowRadius, rightHeight);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.Right], null, right);
|
||||
}
|
||||
|
||||
var bottomLeft = new Rect(shadowBounds.Left, guidelineSetY[3], cornerRadius.BottomLeft,
|
||||
cornerRadius.BottomLeft);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.BottomLeft], null, bottomLeft);
|
||||
var bottomWidth = guidelineSetX[4] - guidelineSetX[3];
|
||||
|
||||
if (bottomWidth > 0d)
|
||||
{
|
||||
var bottom = new Rect(guidelineSetX[3], guidelineSetY[5], bottomWidth, ShadowRadius);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.Bottom], null, bottom);
|
||||
}
|
||||
|
||||
var bottomRight = new Rect(guidelineSetX[4], guidelineSetY[4], cornerRadius.BottomRight,
|
||||
cornerRadius.BottomRight);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.BottomRight], null, bottomRight);
|
||||
|
||||
if (cornerRadius.TopLeft == ShadowRadius && cornerRadius.TopLeft == cornerRadius.TopRight &&
|
||||
cornerRadius.TopLeft == cornerRadius.BottomLeft && cornerRadius.TopLeft == cornerRadius.BottomRight)
|
||||
{
|
||||
var center = new Rect(guidelineSetX[0], guidelineSetY[0], centerWidth, centerHeight);
|
||||
drawingContext.DrawRectangle(brushes[(int)Placement.Center], null, center);
|
||||
}
|
||||
else
|
||||
{
|
||||
var figure = new PathFigure();
|
||||
|
||||
if (cornerRadius.TopLeft > ShadowRadius)
|
||||
{
|
||||
figure.StartPoint = new Point(guidelineSetX[1], guidelineSetY[0]);
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[1], guidelineSetY[1]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[1]), true));
|
||||
}
|
||||
else
|
||||
{
|
||||
figure.StartPoint = new Point(guidelineSetX[0], guidelineSetY[0]);
|
||||
}
|
||||
|
||||
if (cornerRadius.BottomLeft > ShadowRadius)
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[3]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[3]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[5]), true));
|
||||
}
|
||||
else
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[5]), true));
|
||||
}
|
||||
|
||||
if (cornerRadius.BottomRight > ShadowRadius)
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[5]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[4]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[4]), true));
|
||||
}
|
||||
else
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[5]), true));
|
||||
}
|
||||
|
||||
if (cornerRadius.TopRight > ShadowRadius)
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[2]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[2]), true));
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[0]), true));
|
||||
}
|
||||
else
|
||||
{
|
||||
figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[0]), true));
|
||||
}
|
||||
|
||||
figure.IsClosed = true;
|
||||
figure.Freeze();
|
||||
var geometry = new PathGeometry();
|
||||
geometry.Figures.Add(figure);
|
||||
geometry.Freeze();
|
||||
drawingContext.DrawGeometry(brushes[(int)Placement.Center], null, geometry);
|
||||
}
|
||||
|
||||
drawingContext.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearBrushes(DependencyObject o, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((MyDropShadow)o)._brushes = null;
|
||||
}
|
||||
|
||||
private GradientStopCollection CreateStops(Color c, double cornerRadius)
|
||||
{
|
||||
var gradientScale = 1d / (ShadowRadius + cornerRadius);
|
||||
var gsc = new GradientStopCollection();
|
||||
var stopColor = c;
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.1d + cornerRadius) * gradientScale));
|
||||
stopColor.A = (byte)Math.Round(0.74336d * c.A);
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.3d + cornerRadius) * gradientScale));
|
||||
stopColor.A = (byte)Math.Round(0.38053d * c.A);
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.5d + cornerRadius) * gradientScale));
|
||||
stopColor.A = (byte)Math.Round(0.12389d * c.A);
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.7d + cornerRadius) * gradientScale));
|
||||
stopColor.A = (byte)Math.Round(0.02654d * c.A);
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.9d + cornerRadius) * gradientScale));
|
||||
stopColor.A = 0;
|
||||
gsc.Add(new GradientStop(stopColor, (ShadowRadius + cornerRadius) * gradientScale));
|
||||
gsc.Freeze();
|
||||
return gsc;
|
||||
}
|
||||
|
||||
private Brush[] CreateBrushes(Color c, CornerRadius cornerRadius)
|
||||
{
|
||||
var brushes = new Brush[9];
|
||||
brushes[(int)Placement.Center] = new SolidColorBrush(c);
|
||||
brushes[(int)Placement.Center].Freeze();
|
||||
var sideStops = CreateStops(c, 0d);
|
||||
var top = new LinearGradientBrush(sideStops, new Point(0d, 1d), new Point(0d, 0d));
|
||||
top.Freeze();
|
||||
brushes[(int)Placement.Top] = top;
|
||||
var left = new LinearGradientBrush(sideStops, new Point(1d, 0d), new Point(0d, 0d));
|
||||
left.Freeze();
|
||||
brushes[(int)Placement.Left] = left;
|
||||
var right = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(1d, 0d));
|
||||
right.Freeze();
|
||||
brushes[(int)Placement.Right] = right;
|
||||
var bottom = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(0d, 1d));
|
||||
bottom.Freeze();
|
||||
brushes[(int)Placement.Bottom] = bottom;
|
||||
GradientStopCollection topLeftStops;
|
||||
|
||||
if (cornerRadius.TopLeft == 0d)
|
||||
topLeftStops = sideStops;
|
||||
else
|
||||
topLeftStops = CreateStops(c, cornerRadius.TopLeft);
|
||||
|
||||
var topLeft = new RadialGradientBrush(topLeftStops)
|
||||
{
|
||||
RadiusX = 1d,
|
||||
RadiusY = 1d,
|
||||
Center = new Point(1d, 1d),
|
||||
GradientOrigin = new Point(1d, 1d)
|
||||
};
|
||||
topLeft.Freeze();
|
||||
brushes[(int)Placement.TopLeft] = topLeft;
|
||||
GradientStopCollection topRightStops;
|
||||
|
||||
if (cornerRadius.TopRight == 0d)
|
||||
topRightStops = sideStops;
|
||||
else if (cornerRadius.TopRight == cornerRadius.TopLeft)
|
||||
topRightStops = topLeftStops;
|
||||
else
|
||||
topRightStops = CreateStops(c, cornerRadius.TopRight);
|
||||
|
||||
var topRight = new RadialGradientBrush(topRightStops)
|
||||
{
|
||||
RadiusX = 1d,
|
||||
RadiusY = 1d,
|
||||
Center = new Point(0d, 1d),
|
||||
GradientOrigin = new Point(0d, 1d)
|
||||
};
|
||||
topRight.Freeze();
|
||||
brushes[(int)Placement.TopRight] = topRight;
|
||||
GradientStopCollection bottomLeftStops;
|
||||
|
||||
if (cornerRadius.BottomLeft == 0d)
|
||||
bottomLeftStops = sideStops;
|
||||
else if (cornerRadius.BottomLeft == cornerRadius.TopLeft)
|
||||
bottomLeftStops = topLeftStops;
|
||||
else if (cornerRadius.BottomLeft == cornerRadius.TopRight)
|
||||
bottomLeftStops = topRightStops;
|
||||
else
|
||||
bottomLeftStops = CreateStops(c, cornerRadius.BottomLeft);
|
||||
|
||||
var bottomLeft = new RadialGradientBrush(bottomLeftStops)
|
||||
{
|
||||
RadiusX = 1d,
|
||||
RadiusY = 1d,
|
||||
Center = new Point(1d, 0d),
|
||||
GradientOrigin = new Point(1d, 0d)
|
||||
};
|
||||
bottomLeft.Freeze();
|
||||
brushes[(int)Placement.BottomLeft] = bottomLeft;
|
||||
GradientStopCollection bottomRightStops;
|
||||
|
||||
if (cornerRadius.BottomRight == 0d)
|
||||
bottomRightStops = sideStops;
|
||||
else if (cornerRadius.BottomRight == cornerRadius.TopLeft)
|
||||
bottomRightStops = topLeftStops;
|
||||
else if (cornerRadius.BottomRight == cornerRadius.TopRight)
|
||||
bottomRightStops = topRightStops;
|
||||
else if (cornerRadius.BottomRight == cornerRadius.BottomLeft)
|
||||
bottomRightStops = bottomLeftStops;
|
||||
else
|
||||
bottomRightStops = CreateStops(c, cornerRadius.BottomRight);
|
||||
|
||||
var bottomRight = new RadialGradientBrush(bottomRightStops)
|
||||
{
|
||||
RadiusX = 1d,
|
||||
RadiusY = 1d,
|
||||
Center = new Point(0d, 0d),
|
||||
GradientOrigin = new Point(0d, 0d)
|
||||
};
|
||||
bottomRight.Freeze();
|
||||
brushes[(int)Placement.BottomRight] = bottomRight;
|
||||
return brushes;
|
||||
}
|
||||
|
||||
private Brush[] GetBrushes(Color c, CornerRadius cornerRadius)
|
||||
{
|
||||
if (_commonBrushes is null)
|
||||
lock (_resourceAccess)
|
||||
{
|
||||
if (_commonBrushes is null)
|
||||
{
|
||||
_commonBrushes = CreateBrushes(c, cornerRadius);
|
||||
_commonCornerRadius = cornerRadius;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == ((SolidColorBrush)_commonBrushes[(int)Placement.Center]).Color && cornerRadius == _commonCornerRadius)
|
||||
{
|
||||
_brushes = null;
|
||||
return _commonBrushes;
|
||||
}
|
||||
|
||||
if (_brushes is null) _brushes = CreateBrushes(c, cornerRadius);
|
||||
|
||||
return _brushes;
|
||||
}
|
||||
|
||||
private enum Placement
|
||||
{
|
||||
TopLeft = 0,
|
||||
Top = 1,
|
||||
TopRight = 2,
|
||||
Left = 3,
|
||||
Center = 4,
|
||||
Right = 5,
|
||||
BottomLeft = 6,
|
||||
Bottom = 7,
|
||||
BottomRight = 8
|
||||
}
|
||||
}
|
||||
|
||||
// 参考自:https://referencesource.microsoft.com/#PresentationFramework.Aero/parent/Shared/Microsoft/Windows/Themes/SystemDropShadowChrome.cs,6d9c27d92a8128c1
|
||||
@@ -0,0 +1,48 @@
|
||||
<Grid
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
x:Name="PanBack" mc:Ignorable="d"
|
||||
x:Class="PCL.MyExtraButton"
|
||||
Width="40" RenderTransformOrigin="0.5,0.5" ToolTipService.Placement="Left" ToolTipService.VerticalOffset="16"
|
||||
ToolTipService.HorizontalOffset="-8" Height="0">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform ScaleX="0" ScaleY="0" />
|
||||
</Grid.RenderTransform>
|
||||
<Border x:Name="PanClick" CornerRadius="1000" BorderThickness="0.001"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}"
|
||||
MouseLeftButtonUp="Button_LeftMouseUp" MouseLeftButtonDown="Button_LeftMouseDown"
|
||||
MouseRightButtonUp="Button_RightMouseUp" MouseRightButtonDown="Button_RightMouseDown"
|
||||
MouseLeave="PanClick_MouseEvent" MouseEnter="PanClick_MouseEvent" />
|
||||
<Grid x:Name="PanScale" IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5" Margin="0,10,0,0">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Border x:Name="PanColor" CornerRadius="1000" BorderThickness="0.001"
|
||||
Background="{DynamicResource ColorBrush3}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect ShadowDepth="0" BlurRadius="10" Opacity="0.2" />
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border x:Name="PanProgress" CornerRadius="1000" BorderThickness="0.001" Background="#2FFFFFFF"
|
||||
ClipToBounds="True" Visibility="Collapsed">
|
||||
<Border.Clip>
|
||||
<RectangleGeometry x:Name="RectProgress" Rect="0,5,40,35" />
|
||||
</Border.Clip>
|
||||
</Border>
|
||||
<Grid x:Name="IconHost" Margin="12" RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Path x:Name="Path"
|
||||
Stretch="Uniform"
|
||||
Fill="{DynamicResource ColorBrush8}" />
|
||||
<svgIcon:SvgIcon x:Name="ShapeSvgIcon"
|
||||
Stretch="Uniform"
|
||||
Visibility="Collapsed"
|
||||
IconBrush="{Binding Fill, ElementName=Path}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -0,0 +1,323 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyExtraButton
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
|
||||
|
||||
public delegate void RightClickEventHandler(object sender, MouseButtonEventArgs e);
|
||||
|
||||
public delegate bool ShowCheckDelegate();
|
||||
|
||||
// 自定义事件
|
||||
// 务必放在 IsMouseDown 更新之后
|
||||
private const int animationColorIn = 120;
|
||||
private const int animationColorOut = 150;
|
||||
|
||||
// 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
|
||||
private bool isLeftMouseHeld;
|
||||
private bool isRightMouseHeld;
|
||||
public ShowCheckDelegate showCheck = null;
|
||||
|
||||
// 自定义属性
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyExtraButton()
|
||||
{
|
||||
Loaded += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
InitializeComponent();
|
||||
PanClick.MouseLeave += (_, _) => Button_MouseLeave();
|
||||
}
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
if (value < 0.0001d)
|
||||
{
|
||||
PanProgress.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
PanProgress.Visibility = Visibility.Visible;
|
||||
RectProgress.Rect = new Rect(0d, 40d * (1d - value), 40d, 40d * value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Logo
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if ((value ?? "") == (field ?? ""))
|
||||
return;
|
||||
field = value;
|
||||
Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
|
||||
SvgIconControlHelper.ApplyVisibility(Path, ShapeSvgIcon, IsUsingSvgIcon);
|
||||
}
|
||||
} = "";
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
value ??= string.Empty;
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
if (Path is null || ShapeSvgIcon is null)
|
||||
return;
|
||||
SvgIconControlHelper.ApplyIcon(Path, ShapeSvgIcon, field);
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private double EffectiveLogoScale => IsUsingSvgIcon ? 1D : LogoScale;
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
public bool Show
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// 有了
|
||||
Visibility = Visibility.Visible;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(this, 0.3d - ((ScaleTransform)RenderTransform).ScaleX, 500,
|
||||
60, new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaScaleTransform(this, 0.7d, 500, 60,
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaHeight(this, 50d - Height, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyExtraButton MainScale " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没了
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100,
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaHeight(this, -Height, 400, 100, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() => Visibility = Visibility.Collapsed, after: true)
|
||||
}, "MyExtraButton MainScale " + Uuid);
|
||||
}
|
||||
|
||||
IsHitTestVisible = value; // 防止缩放动画中依然可以点进去
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanRightClick { get; set; }
|
||||
|
||||
private void ApplyLogoScale()
|
||||
{
|
||||
IconHost?.RenderTransform = new ScaleTransform { ScaleX = EffectiveLogoScale, ScaleY = EffectiveLogoScale };
|
||||
}
|
||||
|
||||
// 声明
|
||||
public event ClickEventHandler? Click;
|
||||
public event RightClickEventHandler? RightClick;
|
||||
|
||||
private void StartScaleAnimation(double targetScale, double reboundScale, int reboundDuration = 60)
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, targetScale - ((ScaleTransform)PanScale.RenderTransform).ScaleX,
|
||||
800, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
|
||||
ModAnimation.AaScaleTransform(PanScale, reboundScale, reboundDuration,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyExtraButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void RefreshScaleAfterRelease()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
|
||||
ease: new ModAnimation.AniEaseOutBack())
|
||||
}, "MyExtraButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
public void ShowRefresh()
|
||||
{
|
||||
if (showCheck is not null)
|
||||
Show = showCheck();
|
||||
}
|
||||
|
||||
// 触发点击事件
|
||||
private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (isLeftMouseHeld)
|
||||
{
|
||||
ModBase.Log("[Control] 按下附加按钮" +
|
||||
(ToolTip is null or "" ? "" : ":" + ToolTip));
|
||||
Click?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
Button_LeftMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_RightMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (isRightMouseHeld)
|
||||
{
|
||||
ModBase.Log("[Control] 右键按下附加按钮" +
|
||||
(ToolTip is null or "" ? "" : ":" + ToolTip));
|
||||
RightClick?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
Button_RightMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isLeftMouseHeld && !isRightMouseHeld)
|
||||
StartScaleAnimation(0.85d, -0.05d);
|
||||
isLeftMouseHeld = true;
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void Button_RightMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!CanRightClick)
|
||||
return;
|
||||
if (!isLeftMouseHeld && !isRightMouseHeld)
|
||||
StartScaleAnimation(0.85d, -0.05d);
|
||||
isRightMouseHeld = true;
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void Button_LeftMouseUp()
|
||||
{
|
||||
if (!isRightMouseHeld)
|
||||
RefreshScaleAfterRelease();
|
||||
if (isLeftMouseHeld) ModMain.RaiseCustomEvent(this);
|
||||
isLeftMouseHeld = false;
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
private void Button_RightMouseUp()
|
||||
{
|
||||
if (!CanRightClick)
|
||||
return;
|
||||
if (!isLeftMouseHeld)
|
||||
RefreshScaleAfterRelease();
|
||||
isRightMouseHeld = false;
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
private void Button_MouseLeave()
|
||||
{
|
||||
isLeftMouseHeld = false;
|
||||
isRightMouseHeld = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyExtraButton Scale " + Uuid);
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
public void RefreshColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
if (!IsEnabled)
|
||||
// 禁用
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", animationColorIn),
|
||||
"MyExtraButton Color " + Uuid);
|
||||
else if (IsMouseOver)
|
||||
// 指向
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", animationColorIn),
|
||||
"MyExtraButton Color " + Uuid);
|
||||
else
|
||||
// 普通
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", animationColorOut),
|
||||
"MyExtraButton Color " + Uuid);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ControlVisualHelpers.AnimateColorOrSetResource(PanColor, BackgroundProperty,
|
||||
!IsEnabled ? "ColorBrushGray4" : IsMouseOver ? "ColorBrush4" : "ColorBrush3",
|
||||
!IsEnabled || IsMouseOver ? animationColorIn : animationColorOut,
|
||||
"MyExtraButton Color " + Uuid, false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新图标按钮颜色出错");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发出一圈波浪效果提示。
|
||||
/// </summary>
|
||||
public void Ribble()
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
var shape = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(1000d), BorderThickness = new Thickness(0.001d), Opacity = 0.5d,
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d), RenderTransform = new ScaleTransform()
|
||||
};
|
||||
shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5");
|
||||
PanScale.Children.Insert(0, shape);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(shape, 13d, 1000,
|
||||
ease: new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Strong, 0.3d)),
|
||||
ModAnimation.AaOpacity(shape, -shape.Opacity, 1000),
|
||||
ModAnimation.AaCode(() => PanScale.Children.Remove(shape), after: true)
|
||||
}, "ExtraButton Ribble " + ModBase.GetUuid());
|
||||
});
|
||||
}
|
||||
|
||||
private void PanClick_MouseEvent(object sender, MouseEventArgs e)
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Grid
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
x:Name="PanBack" mc:Ignorable="d" x:Class="PCL.MyExtraTextButton"
|
||||
RenderTransformOrigin="0.5,0.5" ToolTipService.Placement="Left" Height="52" Margin="20">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform ScaleX="0" ScaleY="0" />
|
||||
</Grid.RenderTransform>
|
||||
<Border x:Name="PanClick" BorderThickness="0.001" Background="{StaticResource ColorBrushSemiTransparent}"
|
||||
CornerRadius="{Binding ActualHeight, ConverterParameter=0.4, Converter={StaticResource MultiplicationConverter}, ElementName=PanBack, Mode=OneWay}" />
|
||||
<Grid x:Name="PanScale" IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5" Margin="0,10,0,0">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Border x:Name="PanColor" BorderThickness="0.001" Background="{DynamicResource ColorBrush3}"
|
||||
CornerRadius="{Binding ActualHeight, ConverterParameter=0.4, Converter={StaticResource MultiplicationConverter}, ElementName=PanBack, Mode=OneWay}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect ShadowDepth="0" BlurRadius="10" Opacity="0.2" />
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<StackPanel Orientation="Horizontal" Margin="20,0" SnapsToDevicePixels="False" UseLayoutRounding="False">
|
||||
<Grid x:Name="IconHost" Width="16" Margin="2,12,0,12" RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Path x:Name="Path"
|
||||
Stretch="Uniform"
|
||||
Fill="{DynamicResource ColorBrush8}" />
|
||||
<svgIcon:SvgIcon x:Name="ShapeSvgIcon"
|
||||
Stretch="Uniform"
|
||||
Visibility="Collapsed"
|
||||
IconBrush="{Binding Fill, ElementName=Path}" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="LabText" VerticalAlignment="Center" Foreground="{DynamicResource ColorBrush8}"
|
||||
FontSize="16"
|
||||
Margin="12,0,0,0.8" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -0,0 +1,283 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyExtraTextButton
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
|
||||
|
||||
// 自定义事件
|
||||
// 务必放在 IsMouseDown 更新之后
|
||||
private const int animationColorIn = 120;
|
||||
private const int animationColorOut = 150;
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyExtraTextButton), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
((MyExtraTextButton)sender)?.LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
// 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
|
||||
private bool isLeftMouseHeld;
|
||||
|
||||
// 自定义属性
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyExtraTextButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
RefreshIconHostVisibility();
|
||||
|
||||
Loaded += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
PanClick.MouseLeftButtonDown += Button_LeftMouseDown;
|
||||
PanClick.MouseLeftButtonUp += Button_LeftMouseUp;
|
||||
PanClick.MouseLeave += Button_MouseLeave;
|
||||
PanClick.MouseRightButtonUp += Button_RightMouseUp;
|
||||
PanClick.MouseEnter += (sender, e) => RefreshColor();
|
||||
}
|
||||
|
||||
public string Logo
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if ((value ?? "") == (field ?? ""))
|
||||
return;
|
||||
field = value ?? string.Empty;
|
||||
Path.Data = string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
: (Geometry)new GeometryConverter().ConvertFromString(value);
|
||||
SvgIconControlHelper.ApplyVisibility(Path, ShapeSvgIcon, IsUsingSvgIcon);
|
||||
RefreshIconHostVisibility();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
value ??= string.Empty;
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
if (Path is null || ShapeSvgIcon is null)
|
||||
return;
|
||||
SvgIconControlHelper.ApplyIcon(Path, ShapeSvgIcon, field);
|
||||
ApplyLogoScale();
|
||||
RefreshIconHostVisibility();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private double EffectiveLogoScale => IsUsingSvgIcon ? 1D : LogoScale;
|
||||
|
||||
private bool HasAnyIcon => IsUsingSvgIcon || !string.IsNullOrWhiteSpace(Logo);
|
||||
|
||||
private void ApplyLogoScale()
|
||||
{
|
||||
IconHost?.RenderTransform = new ScaleTransform
|
||||
{
|
||||
ScaleX = EffectiveLogoScale,
|
||||
ScaleY = EffectiveLogoScale
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshIconHostVisibility()
|
||||
{
|
||||
if (IconHost is null || LabText is null)
|
||||
return;
|
||||
|
||||
if (HasAnyIcon)
|
||||
{
|
||||
IconHost.Visibility = Visibility.Visible;
|
||||
IconHost.Width = 16;
|
||||
IconHost.Margin = new Thickness(2, 12, 0, 12);
|
||||
LabText.Margin = new Thickness(12, 0, 0, 0.8);
|
||||
}
|
||||
else
|
||||
{
|
||||
IconHost.Visibility = Visibility.Collapsed;
|
||||
IconHost.Width = 0;
|
||||
IconHost.Margin = new Thickness(0, 12, 0, 12);
|
||||
LabText.Margin = new Thickness(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
// 显示文本
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set
|
||||
{
|
||||
if (value is null) return;
|
||||
SetValue(TextProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Show
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// 有了
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d - Opacity, 80, 50),
|
||||
ModAnimation.AaScaleTransform(this, 0.15d - ((ScaleTransform)RenderTransform).ScaleX, 400,
|
||||
50, new ModAnimation.AniEaseOutBack()),
|
||||
ModAnimation.AaScaleTransform(this, 0.85d, 160, 50, new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyExtraTextButton MainScale " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没了
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, -Opacity, 50, 50),
|
||||
ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100,
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyExtraTextButton MainScale " + Uuid);
|
||||
}
|
||||
|
||||
IsHitTestVisible = value; // 防止缩放动画中依然可以点进去
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 声明
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
private void StartScaleAnimation(double targetScale, double reboundScale, int reboundDuration = 60)
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, targetScale - ((ScaleTransform)PanScale.RenderTransform).ScaleX,
|
||||
800, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
|
||||
ModAnimation.AaScaleTransform(PanScale, reboundScale, reboundDuration,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyExtraTextButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void RefreshScaleAfterRelease()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
|
||||
ease: new ModAnimation.AniEaseOutBack())
|
||||
}, "MyExtraTextButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
// 触发点击事件
|
||||
private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isLeftMouseHeld) return;
|
||||
ModBase.Log("[Control] 按下附加图标按钮:" + Text);
|
||||
Click?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
Button_LeftMouseUp();
|
||||
}
|
||||
|
||||
private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isLeftMouseHeld)
|
||||
StartScaleAnimation(0.85d, -0.05d);
|
||||
isLeftMouseHeld = true;
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void Button_LeftMouseUp()
|
||||
{
|
||||
RefreshScaleAfterRelease();
|
||||
isLeftMouseHeld = false;
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
private void Button_RightMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!isLeftMouseHeld)
|
||||
RefreshScaleAfterRelease();
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
private void Button_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
isLeftMouseHeld = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyExtraTextButton Scale " + Uuid);
|
||||
RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
public void RefreshColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
if (!IsEnabled)
|
||||
// 禁用
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", animationColorIn),
|
||||
"MyExtraTextButton Color " + Uuid);
|
||||
else if (IsMouseOver)
|
||||
// 指向
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", animationColorIn),
|
||||
"MyExtraTextButton Color " + Uuid);
|
||||
else
|
||||
// 普通
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", animationColorOut),
|
||||
"MyExtraTextButton Color " + Uuid);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ControlVisualHelpers.AnimateColorOrSetResource(PanColor, BackgroundProperty,
|
||||
!IsEnabled ? "ColorBrushGray4" : IsMouseOver ? "ColorBrush4" : "ColorBrush3",
|
||||
!IsEnabled || IsMouseOver ? animationColorIn : animationColorOut,
|
||||
"MyExtraTextButton Color " + Uuid, false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新附加图标按钮颜色出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Border x:Class="PCL.MyHint" x:Name="PanBack"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
UseLayoutRounding="True" SnapsToDevicePixels="True" HorizontalAlignment="Stretch" BorderThickness="3,0,0,0"
|
||||
BorderBrush="#99FF4444" CornerRadius="2">
|
||||
<Grid UseLayoutRounding="False" SnapsToDevicePixels="False">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock IsHitTestVisible="False" x:Name="LabText" LineHeight="16" Margin="0,0,0,-1" Padding="12,9"
|
||||
VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||
<local:MyIconButton x:Name="BtnClose" Visibility="Collapsed" Theme="Custom" Margin="0,0,8,0" Grid.Column="1" Click="BtnClose_Click"
|
||||
Height="20" Width="20" VerticalAlignment="Center"
|
||||
SvgIcon="lucide/x"
|
||||
ToolTip="{DynamicResource Common.Hint.DoNotShowAgain}" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="29"
|
||||
ToolTipService.HorizontalOffset="2" />
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -0,0 +1,204 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Configuration;
|
||||
using PCL.Core.UI.Theme;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyHint
|
||||
{
|
||||
// 配色
|
||||
public enum Themes
|
||||
{
|
||||
Blue = 0,
|
||||
Red = 1,
|
||||
Yellow = 2
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsWarnProperty = DependencyProperty.Register("IsWarn", typeof(bool),
|
||||
typeof(MyHint),
|
||||
new PropertyMetadata(true,
|
||||
(d, e) =>
|
||||
{
|
||||
var f = (MyHint)d;
|
||||
f.Theme = e.NewValue is not null ? Themes.Red : Themes.Blue;
|
||||
}));
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyHint), new PropertyMetadata("", (d, e) =>
|
||||
{
|
||||
var f = (MyHint)d;
|
||||
f.LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
// 触发点击事件
|
||||
private bool isMouseDown;
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyHint()
|
||||
{
|
||||
InitializeComponent();
|
||||
UpdateUI();
|
||||
Loaded += (_, _) => UpdateUI();
|
||||
Loaded += MyHint_Loaded;
|
||||
MouseLeftButtonUp += MyHint_MouseUp;
|
||||
MouseLeftButtonDown += MyHint_MouseDown;
|
||||
MouseLeave += (_, _) => MyHint_MouseLeave();
|
||||
Unloaded += (_, _) => Dispose();
|
||||
}
|
||||
|
||||
// 边框
|
||||
public bool HasBorder
|
||||
{
|
||||
get => BorderThickness.Top > 0d;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
BorderThickness = new Thickness(3d, ModBase.GetWPFSize(1d), ModBase.GetWPFSize(1d), ModBase.GetWPFSize(1d));
|
||||
else
|
||||
BorderThickness = new Thickness(3d, 0d, 0d, 0d);
|
||||
}
|
||||
}
|
||||
|
||||
public Themes Theme
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
UpdateUI();
|
||||
}
|
||||
} = Themes.Red;
|
||||
|
||||
[Obsolete("IsWarn 已过时。请换用 Theme 属性。")]
|
||||
public bool IsWarn
|
||||
{
|
||||
get => Theme == Themes.Red;
|
||||
set => Theme = value ? Themes.Red : Themes.Blue;
|
||||
}
|
||||
|
||||
// 文本
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
public bool CanClose
|
||||
{
|
||||
get => BtnClose.Visibility == Visibility.Visible;
|
||||
set => BtnClose.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public string RelativeSetup { get; set; } = "";
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
var hue = default(double);
|
||||
switch (Theme)
|
||||
{
|
||||
case Themes.Blue:
|
||||
{
|
||||
hue = 210d;
|
||||
break;
|
||||
}
|
||||
case Themes.Red:
|
||||
{
|
||||
hue = 355d;
|
||||
break;
|
||||
}
|
||||
case Themes.Yellow:
|
||||
{
|
||||
hue = 40d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var s = ThemeService.CurrentTone;
|
||||
Background = new ModBase.MyColor().FromHSL2(hue, 90, s.L7 * 100);
|
||||
BorderBrush = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
|
||||
LabText.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
|
||||
BtnClose.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
|
||||
|
||||
// 根据提示气泡对齐方向刷新边框
|
||||
// 此处依赖 HasBorder 的副作用进行范围检查
|
||||
HasBorder = HasBorder;
|
||||
}
|
||||
|
||||
private void MyHint_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeService.ColorModeChanged += (v, theme) => _ThemeChanged(v, theme);
|
||||
if (CanClose && ConfigService.TryGetConfigItemNoType(RelativeSetup, out var item) && item.GetValueNoType() is not null)
|
||||
Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (ConfigService.TryGetConfigItemNoType(RelativeSetup, out var item))
|
||||
item.SetValueNoType(true);
|
||||
ModAnimation.AniDispose(this, false);
|
||||
}
|
||||
|
||||
private void MyHint_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
isMouseDown = false;
|
||||
ModBase.Log("[Control] 按下提示条" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name));
|
||||
e.Handled = true;
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
private void MyHint_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isMouseDown = true;
|
||||
}
|
||||
|
||||
private void MyHint_MouseLeave()
|
||||
{
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
private void _ThemeChanged(bool isDarkMode, ColorTheme theme)
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void Dispose()
|
||||
{
|
||||
ThemeService.ColorModeChanged -= _ThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class ModAnimation
|
||||
{
|
||||
public static void AniDispose(MyHint control, bool removeFromChildren, ParameterizedThreadStart callBack = null)
|
||||
{
|
||||
if (!control.IsHitTestVisible)
|
||||
return;
|
||||
control.IsHitTestVisible = false;
|
||||
AniStart(new[]
|
||||
{
|
||||
AaScaleTransform(control, -0.08d, 200, ease: new AniEaseInFluent()),
|
||||
AaOpacity(control, -1, 200, ease: new AniEaseOutFluent()),
|
||||
AaHeight(control, -control.ActualHeight, 150, 100, new AniEaseOutFluent()),
|
||||
AaCode(() =>
|
||||
{
|
||||
if (removeFromChildren)
|
||||
((Panel)control.Parent).Children.Remove(control);
|
||||
else
|
||||
control.Visibility = Visibility.Collapsed;
|
||||
if (callBack is not null)
|
||||
callBack(control);
|
||||
}, after: true)
|
||||
}, "MyCard Dispose " + control.Uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Border x:Class="PCL.MyIconButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
CornerRadius="1000" Background="{StaticResource ColorBrushSemiTransparent}" BorderThickness="0.001">
|
||||
<Border x:Name="PanBack" CornerRadius="1000" RenderTransformOrigin="0.5,0.5" BorderThickness="0.001"
|
||||
IsHitTestVisible="False">
|
||||
<Grid x:Name="IconHost" Margin="5" RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Path x:Name="Path"
|
||||
Stretch="Uniform"
|
||||
RenderTransformOrigin="0.5,0.5" />
|
||||
<svgIcon:SvgIcon x:Name="ShapeSvgIcon"
|
||||
Stretch="Uniform"
|
||||
Visibility="Collapsed"
|
||||
IconBrush="{Binding Fill, ElementName=Path}" />
|
||||
</Grid>
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
</Border>
|
||||
@@ -0,0 +1,455 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyIconButton
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, EventArgs e);
|
||||
|
||||
public enum Themes
|
||||
{
|
||||
Color,
|
||||
White,
|
||||
Black,
|
||||
Red,
|
||||
Custom
|
||||
}
|
||||
|
||||
// 务必放在 IsMouseDown 更新之后
|
||||
private const int animationColorIn = 120;
|
||||
private const int animationColorOut = 150;
|
||||
|
||||
//鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
|
||||
private bool isMouseDown;
|
||||
|
||||
// 自定义属性
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
protected override Size MeasureOverride(Size constraint)
|
||||
{
|
||||
var measured = base.MeasureOverride(constraint);
|
||||
if (double.IsNaN(Width) && !double.IsNaN(Height) && Height > 0D && !double.IsInfinity(Height))
|
||||
return new Size(Height, Height);
|
||||
return measured;
|
||||
}
|
||||
|
||||
public MyIconButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MouseLeftButtonUp += Button_MouseUp;
|
||||
MouseLeftButtonDown += Button_MouseDown;
|
||||
MouseLeftButtonUp += (_, _) => Button_MouseUp();
|
||||
MouseLeave += (_, _) => Button_MouseLeave();
|
||||
MouseEnter += (_, _) => RefreshAnim();
|
||||
MouseLeave += (_, _) => RefreshAnim();
|
||||
Loaded += (_, _) => RefreshAnim();
|
||||
}
|
||||
|
||||
public string Logo
|
||||
{
|
||||
get => Path.Data?.ToString() ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (Path is null) return;
|
||||
Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
|
||||
SvgIconControlHelper.ApplyVisibility(Path, ShapeSvgIcon, IsUsingSvgIcon);
|
||||
}
|
||||
}
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
value ??= string.Empty;
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
if (Path is null || ShapeSvgIcon is null)
|
||||
return;
|
||||
SvgIconControlHelper.ApplyIcon(Path, ShapeSvgIcon, field);
|
||||
ApplyLogoScale();
|
||||
RefreshAnim();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private double EffectiveLogoScale => IsUsingSvgIcon ? 1D : LogoScale;
|
||||
|
||||
private void ApplyLogoScale()
|
||||
{
|
||||
IconHost?.RenderTransform = new ScaleTransform { ScaleX = EffectiveLogoScale, ScaleY = EffectiveLogoScale };
|
||||
}
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
public Themes Theme { get; set; } = Themes.Color;
|
||||
|
||||
public SolidColorBrush Foreground
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
RefreshAnim();
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
}
|
||||
} = new(Color.FromRgb(128, 128, 128));
|
||||
|
||||
private string ColorAnimationKey => "MyIconButton Color " + Uuid;
|
||||
|
||||
// 自定义事件
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
private static ModBase.MyColor GetTransparentBackground()
|
||||
{
|
||||
return new ModBase.MyColor(0d, 255d, 255d, 255d);
|
||||
}
|
||||
|
||||
private ModBase.MyColor? GetBaseFillColor()
|
||||
{
|
||||
return Theme switch
|
||||
{
|
||||
Themes.Red => new ModBase.MyColor(160d, 255d, 76d, 76d),
|
||||
Themes.Black => ThemeManager.IsDarkMode
|
||||
? new ModBase.MyColor(160d, 255d, 255d, 255d)
|
||||
: new ModBase.MyColor(160d, 0d, 0d, 0d),
|
||||
Themes.Custom => new ModBase.MyColor(160d, Foreground),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsureBaseBrushes()
|
||||
{
|
||||
PanBack.Background ??= GetTransparentBackground();
|
||||
var baseFill = GetBaseFillColor();
|
||||
if (baseFill is not null && !IsUsingSvgIcon)
|
||||
Path.Fill ??= baseFill;
|
||||
}
|
||||
|
||||
private void AnimateActiveSvgIconBrush(string resourceKey, int duration)
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
SvgIconControlHelper.AnimateSvgIconBrushTo(ShapeSvgIcon, resourceKey, duration, ColorAnimationKey);
|
||||
}
|
||||
|
||||
private void AnimateActiveSvgIconBrush(ModBase.MyColor color, int duration)
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
SvgIconControlHelper.AnimateSvgIconBrushTo(ShapeSvgIcon, color, duration, ColorAnimationKey);
|
||||
}
|
||||
|
||||
private void SetActiveIconResource(string resourceKey)
|
||||
{
|
||||
SvgIconControlHelper.SetIconResource(Path, ShapeSvgIcon, IsUsingSvgIcon, resourceKey);
|
||||
}
|
||||
|
||||
private void SetActiveIconBrush(Brush brush)
|
||||
{
|
||||
SvgIconControlHelper.SetIconBrush(Path, ShapeSvgIcon, IsUsingSvgIcon, brush);
|
||||
}
|
||||
|
||||
private List<ModAnimation.AniData> GetHoverAnimations()
|
||||
{
|
||||
var animations = new List<ModAnimation.AniData>();
|
||||
switch (Theme)
|
||||
{
|
||||
case Themes.Color:
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush("ColorBrush2", animationColorIn);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
"ColorBrush2",
|
||||
animationColorIn));
|
||||
|
||||
break;
|
||||
}
|
||||
case Themes.White:
|
||||
{
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
PanBack,
|
||||
BackgroundProperty,
|
||||
new ModBase.MyColor(50d, 255d, 255d, 255d) - PanBack.Background,
|
||||
animationColorIn));
|
||||
break;
|
||||
}
|
||||
case Themes.Red:
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(
|
||||
new ModBase.MyColor(255d, 76d, 76d),
|
||||
animationColorIn);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
new ModBase.MyColor(255d, 76d, 76d) - Path.Fill,
|
||||
animationColorIn));
|
||||
break;
|
||||
}
|
||||
case Themes.Black:
|
||||
{
|
||||
var blackHoverColor = ThemeManager.IsDarkMode
|
||||
? new ModBase.MyColor(230d, 255d, 255d, 255d)
|
||||
: new ModBase.MyColor(230d, 0d, 0d, 0d);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(blackHoverColor, animationColorIn);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
blackHoverColor - Path.Fill,
|
||||
animationColorIn));
|
||||
break;
|
||||
}
|
||||
case Themes.Custom:
|
||||
{
|
||||
var customHoverColor = new ModBase.MyColor(255d, Foreground);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(customHoverColor, animationColorIn);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
customHoverColor - Path.Fill,
|
||||
animationColorIn));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
private List<ModAnimation.AniData> GetNormalAnimations()
|
||||
{
|
||||
var animations = new List<ModAnimation.AniData>();
|
||||
switch (Theme)
|
||||
{
|
||||
case Themes.Color:
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush("ColorBrush4", animationColorOut);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
"ColorBrush4",
|
||||
animationColorOut));
|
||||
|
||||
PanBack.Background = GetTransparentBackground();
|
||||
break;
|
||||
}
|
||||
case Themes.White:
|
||||
{
|
||||
var whiteNormalColor = new ModBase.MyColor(234d, 242d, 254d);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(whiteNormalColor, animationColorOut);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
whiteNormalColor,
|
||||
animationColorOut));
|
||||
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
PanBack,
|
||||
BackgroundProperty,
|
||||
GetTransparentBackground() - PanBack.Background,
|
||||
animationColorOut));
|
||||
break;
|
||||
}
|
||||
case Themes.Red:
|
||||
{
|
||||
var redNormalColor = new ModBase.MyColor(160d, 255d, 76d, 76d);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(redNormalColor, animationColorOut);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
redNormalColor - Path.Fill,
|
||||
animationColorOut));
|
||||
|
||||
PanBack.Background = GetTransparentBackground();
|
||||
break;
|
||||
}
|
||||
case Themes.Black:
|
||||
{
|
||||
var blackNormalColor = ThemeManager.IsDarkMode
|
||||
? new ModBase.MyColor(160d, 255d, 255d, 255d)
|
||||
: new ModBase.MyColor(160d, 0d, 0d, 0d);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(blackNormalColor, animationColorOut);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
blackNormalColor - Path.Fill,
|
||||
animationColorOut));
|
||||
|
||||
PanBack.Background = GetTransparentBackground();
|
||||
break;
|
||||
}
|
||||
case Themes.Custom:
|
||||
{
|
||||
var customNormalColor = new ModBase.MyColor(160d, Foreground);
|
||||
if (IsUsingSvgIcon)
|
||||
AnimateActiveSvgIconBrush(customNormalColor, animationColorOut);
|
||||
else
|
||||
animations.Add(ModAnimation.AaColor(
|
||||
Path,
|
||||
Shape.FillProperty,
|
||||
customNormalColor - Path.Fill,
|
||||
animationColorOut));
|
||||
|
||||
PanBack.Background = GetTransparentBackground();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
private void ApplyNonAnimatedTheme()
|
||||
{
|
||||
switch (Theme)
|
||||
{
|
||||
case Themes.Color:
|
||||
SetActiveIconResource("ColorBrush4");
|
||||
break;
|
||||
case Themes.White:
|
||||
SetActiveIconBrush(new ModBase.MyColor(234d, 242d, 254d));
|
||||
break;
|
||||
case Themes.Red:
|
||||
SetActiveIconBrush(new ModBase.MyColor(160d, 255d, 76d, 76d));
|
||||
break;
|
||||
case Themes.Black:
|
||||
SetActiveIconBrush(ThemeManager.IsDarkMode
|
||||
? new ModBase.MyColor(160d, 255d, 255d, 255d)
|
||||
: new ModBase.MyColor(160d, 0d, 0d, 0d));
|
||||
break;
|
||||
case Themes.Custom:
|
||||
SetActiveIconBrush(new ModBase.MyColor(160d, Foreground));
|
||||
break;
|
||||
}
|
||||
|
||||
PanBack.Background = GetTransparentBackground();
|
||||
}
|
||||
|
||||
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下图标按钮" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name));
|
||||
Click?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
Button_MouseUp();
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isMouseDown = true;
|
||||
Focus();
|
||||
// 指向
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(PanBack, 0.8d - ((ScaleTransform)PanBack.RenderTransform).ScaleX,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
|
||||
"MyIconButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
private void Button_MouseUp()
|
||||
{
|
||||
if (isMouseDown)
|
||||
{
|
||||
isMouseDown = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanBack, 1.05d - ((ScaleTransform)PanBack.RenderTransform).ScaleX,
|
||||
250, ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaScaleTransform(PanBack, -0.05d, 250,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong))
|
||||
}, "MyIconButton Scale " + Uuid);
|
||||
}
|
||||
|
||||
RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
private void Button_MouseLeave()
|
||||
{
|
||||
isMouseDown = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(PanBack, 1d - ((ScaleTransform)PanBack.RenderTransform).ScaleX, 250,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
}, "MyIconButton Scale " + Uuid);
|
||||
RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave
|
||||
}
|
||||
|
||||
public void RefreshAnim()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
EnsureBaseBrushes();
|
||||
ModAnimation.AniStart(IsMouseOver ? GetHoverAnimations() : GetNormalAnimations(), ColorAnimationKey);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ModAnimation.AniStop(ColorAnimationKey);
|
||||
ApplyNonAnimatedTheme();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新图标按钮动画状态出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class ModAnimation
|
||||
{
|
||||
public static void AniDispose(MyIconButton control, bool removeFromChildren,
|
||||
ParameterizedThreadStart callBack = null)
|
||||
{
|
||||
if (!control.IsHitTestVisible)
|
||||
return;
|
||||
control.IsHitTestVisible = false;
|
||||
AniStart(new[]
|
||||
{
|
||||
AaScaleTransform(control, -1.5d, 200, ease: new AniEaseInFluent()),
|
||||
AaCode(() =>
|
||||
{
|
||||
if (removeFromChildren)
|
||||
((Panel)control.Parent).Children.Remove(control);
|
||||
else
|
||||
control.Visibility = Visibility.Collapsed;
|
||||
if (callBack is not null)
|
||||
callBack(control);
|
||||
}, after: true)
|
||||
}, "MyIconButton Dispose " + control.Uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Border x:Name="PanBack" x:Class="PCL.MyIconTextButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Background="{StaticResource ColorBrushSemiTransparent}"
|
||||
CornerRadius="13.5" MinHeight="27" MaxHeight="27">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid x:Name="LogoHost"
|
||||
Width="16" Height="16"
|
||||
Margin="12,0,0,0"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Path x:Name="ShapeLogo"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Stretch="Uniform"
|
||||
Fill="White" />
|
||||
<svgIcon:SvgIcon x:Name="ShapeSvgIcon"
|
||||
Stretch="Uniform"
|
||||
Visibility="Collapsed"
|
||||
IconBrush="{Binding Fill, ElementName=ShapeLogo}" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="LabText"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White"
|
||||
Margin="7,0,12,1"
|
||||
SnapsToDevicePixels="False"
|
||||
UseLayoutRounding="False" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -0,0 +1,273 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyIconTextButton
|
||||
{
|
||||
public delegate void ChangeEventHandler(object sender, bool raiseByMouse);
|
||||
|
||||
public delegate void CheckEventHandler(object sender, bool raiseByMouse);
|
||||
|
||||
public delegate void ClickEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
public enum ColorState
|
||||
{
|
||||
Black,
|
||||
Highlight
|
||||
}
|
||||
|
||||
// 动画
|
||||
|
||||
private const int animationTimeOfMouseIn = 100; // 鼠标指向动画长度
|
||||
private const int animationTimeOfMouseOut = 150; // 鼠标移出动画长度
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyIconTextButton), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyIconTextButton)sender).LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
public static readonly DependencyProperty ColorTypeProperty = DependencyProperty.Register("ColorType",
|
||||
typeof(ColorState), typeof(MyIconTextButton), new PropertyMetadata(ColorState.Black));
|
||||
|
||||
private bool _hasLegacyLogo;
|
||||
private bool isMouseDown;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyIconTextButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
RefreshLogoHostVisibility();
|
||||
|
||||
MouseLeftButtonUp += (_, _) => MyIconTextButton_MouseUp();
|
||||
MouseLeftButtonDown += (_, _) => MyIconTextButton_MouseDown();
|
||||
MouseLeave += (_, _) => MyIconTextButton_MouseLeave();
|
||||
MouseEnter += RefreshColor;
|
||||
Loaded += RefreshColor;
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
}
|
||||
|
||||
// 自定义属性
|
||||
|
||||
public string Logo
|
||||
{
|
||||
get => ShapeLogo.Data?.ToString() ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (ShapeLogo is null) return;
|
||||
_hasLegacyLogo = !string.IsNullOrWhiteSpace(value);
|
||||
ShapeLogo.Data = _hasLegacyLogo
|
||||
? (Geometry)new GeometryConverter().ConvertFromString(value)!
|
||||
: null;
|
||||
SvgIconControlHelper.ApplyVisibility(ShapeLogo, ShapeSvgIcon, IsUsingSvgIcon);
|
||||
RefreshLogoHostVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
value ??= string.Empty;
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
if (ShapeLogo is null || ShapeSvgIcon is null)
|
||||
return;
|
||||
SvgIconControlHelper.ApplyIcon(ShapeLogo, ShapeSvgIcon, field);
|
||||
ApplyLogoScale();
|
||||
RefreshLogoHostVisibility();
|
||||
RefreshColor();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private double EffectiveLogoScale => IsUsingSvgIcon ? 1D : LogoScale;
|
||||
|
||||
private bool HasAnyIcon => IsUsingSvgIcon || _hasLegacyLogo;
|
||||
|
||||
private void ApplyLogoScale()
|
||||
{
|
||||
LogoHost?.RenderTransform = new ScaleTransform
|
||||
{
|
||||
ScaleX = EffectiveLogoScale,
|
||||
ScaleY = EffectiveLogoScale
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshLogoHostVisibility()
|
||||
{
|
||||
if (LogoHost is null || LabText is null)
|
||||
return;
|
||||
|
||||
if (HasAnyIcon)
|
||||
{
|
||||
LogoHost.Visibility = Visibility.Visible;
|
||||
LogoHost.Width = 16;
|
||||
LogoHost.Height = 16;
|
||||
LogoHost.Margin = new Thickness(12, 0, 0, 0);
|
||||
LabText.Margin = new Thickness(7, 0, 12, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoHost.Visibility = Visibility.Collapsed;
|
||||
LogoHost.Width = 0;
|
||||
LogoHost.Height = 16;
|
||||
LogoHost.Margin = new Thickness(0);
|
||||
LabText.Margin = new Thickness(12, 0, 12, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
} // 内容
|
||||
|
||||
public ColorState ColorType
|
||||
{
|
||||
get => (ColorState)GetValue(ColorTypeProperty);
|
||||
set
|
||||
{
|
||||
if (ColorType == value)
|
||||
return;
|
||||
SetValue(ColorTypeProperty, value);
|
||||
RefreshColor();
|
||||
}
|
||||
} // 颜色类别
|
||||
|
||||
public event CheckEventHandler? Check;
|
||||
public event ChangeEventHandler? Change;
|
||||
|
||||
private string CheckedAnimationKey => "MyIconTextButton Checked " + Uuid;
|
||||
private string ColorAnimationKey => "MyIconTextButton Color " + Uuid;
|
||||
|
||||
// 点击事件
|
||||
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
private string GetDefaultForegroundResourceKey()
|
||||
{
|
||||
return ColorType == ColorState.Highlight ? "ColorBrush3" : "ColorBrush1";
|
||||
}
|
||||
|
||||
private void StartForegroundAnimation(string resourceKey, int duration)
|
||||
{
|
||||
if (IsUsingSvgIcon)
|
||||
{
|
||||
SvgIconControlHelper.AnimateSvgIconBrushTo(ShapeSvgIcon, resourceKey, duration, CheckedAnimationKey);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, resourceKey, duration),
|
||||
CheckedAnimationKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, resourceKey, duration),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, resourceKey, duration)
|
||||
}, CheckedAnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartBackgroundAnimation(string resourceKey, int duration)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, resourceKey, duration), ColorAnimationKey);
|
||||
}
|
||||
|
||||
private void StartBackgroundAnimation(ModBase.MyColor delta, int duration)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, delta, duration), ColorAnimationKey);
|
||||
}
|
||||
|
||||
private void MyIconTextButton_MouseUp()
|
||||
{
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下带图标按钮:" + Text);
|
||||
isMouseDown = false;
|
||||
Click?.Invoke(this, new ModBase.RouteEventArgs(true));
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
private void MyIconTextButton_MouseDown()
|
||||
{
|
||||
isMouseDown = true;
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
private void MyIconTextButton_MouseLeave()
|
||||
{
|
||||
isMouseDown = false;
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
private void RefreshColor(object? obj = null, object? e = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this, e)) // 防止默认属性变更触发动画,若强制不执行动画,则 e 为 False
|
||||
{
|
||||
if (isMouseDown)
|
||||
{
|
||||
StartBackgroundAnimation("ColorBrush6", 70);
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
StartForegroundAnimation("ColorBrush3", animationTimeOfMouseIn);
|
||||
StartBackgroundAnimation("ColorBrushBg1", animationTimeOfMouseIn);
|
||||
}
|
||||
else if (IsEnabled)
|
||||
{
|
||||
StartForegroundAnimation(GetDefaultForegroundResourceKey(), animationTimeOfMouseOut);
|
||||
StartBackgroundAnimation(ThemeManager.colorSemiTransparent - Background, animationTimeOfMouseOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartForegroundAnimation("ColorBrushGray5", 100);
|
||||
StartBackgroundAnimation(ThemeManager.colorSemiTransparent - Background, animationTimeOfMouseOut);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// 不使用动画
|
||||
ModAnimation.AniStop(CheckedAnimationKey);
|
||||
ModAnimation.AniStop(ColorAnimationKey);
|
||||
Background = ThemeManager.colorSemiTransparent;
|
||||
var foregroundKey = IsEnabled ? GetDefaultForegroundResourceKey() : "ColorBrushGray5";
|
||||
SvgIconControlHelper.SetIconResource(ShapeLogo, ShapeSvgIcon, IsUsingSvgIcon, foregroundKey);
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, foregroundKey);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新带图标按钮颜色出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyImage : Image
|
||||
{
|
||||
public MyImage()
|
||||
{
|
||||
Initialized += (_, _) => Load();
|
||||
SizeChanged += (_, _) => UpdateClip();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实际被呈现的图片地址。
|
||||
/// </summary>
|
||||
public string ActualSource
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
value = null;
|
||||
if ((field ?? "") == (value ?? ""))
|
||||
return;
|
||||
field = value;
|
||||
Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ImageSource bitmap = value is null ? null : await Task.Run(() => new MyBitmap(value));
|
||||
base.Source = bitmap;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"加载图片失败({value})");
|
||||
try
|
||||
{
|
||||
if (value.StartsWithF(ModBase.pathTemp) && File.Exists(value)) File.Delete(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
})); // 在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void Load() // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
|
||||
{
|
||||
if (Source is null)
|
||||
{
|
||||
ActualSource = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Source.StartsWithF("http"))
|
||||
{
|
||||
ActualSource = Source;
|
||||
return;
|
||||
}
|
||||
|
||||
var url = Source;
|
||||
var tempPath = GetTempPath(url);
|
||||
var tempFile = new FileInfo(tempPath);
|
||||
var enableCache = this.EnableCache;
|
||||
if (enableCache && tempFile.Exists)
|
||||
{
|
||||
ActualSource = tempPath;
|
||||
if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime)
|
||||
return; // 无需刷新缓存
|
||||
}
|
||||
|
||||
Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 下载
|
||||
ActualSource = LoadingSource;
|
||||
|
||||
var resp = await DownloadImageAsync(url);
|
||||
if (!string.IsNullOrEmpty(resp))
|
||||
{
|
||||
ActualSource = resp;
|
||||
return;
|
||||
}
|
||||
|
||||
resp = await DownloadImageAsync(FallbackSource);
|
||||
if (!string.IsNullOrEmpty(resp))
|
||||
{
|
||||
ActualSource = resp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 更换备用地址
|
||||
ModBase.Log(ex, $"Online image get fail(source = {url}, fallback = {FallbackSource})", ModBase.LogLevel.Developer);
|
||||
tempPath = GetTempPath(url);
|
||||
tempFile = new FileInfo(tempPath);
|
||||
if (enableCache && tempFile.Exists)
|
||||
{
|
||||
ActualSource = tempPath;
|
||||
if (DateTime.Now - tempFile.LastWriteTime < fileCacheExpiredTime)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static Task<string> DownloadImageAsync(string url)
|
||||
{
|
||||
return _downloadTasks.GetOrAdd(url, key =>
|
||||
{
|
||||
var t = DownloadImageInternalAsync(key);
|
||||
t.ContinueWith(_ => _downloadTasks.TryRemove(url, out _));
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
public static string GetTempPath(string url)
|
||||
{
|
||||
return Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{ModBase.GetStringMD5(url)}.png");
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Task<string>> _downloadTasks = new();
|
||||
|
||||
private static async Task<string> DownloadImageInternalAsync(string url)
|
||||
{
|
||||
var tempPath = GetTempPath(url);
|
||||
var tempDownloadingPath = tempPath + RandomUtils.NextInt(0, 1000000);
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ModBase.GetPathFromFullPath(tempPath)); // 重新实现下载,以避免携带 Header(#5072)
|
||||
using (var fs = new FileStream(tempDownloadingPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
|
||||
{
|
||||
using (var response = await HttpRequest.Create(url)
|
||||
.SendAsync(addMetedata: false))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var nfs = await response.AsStreamAsync())
|
||||
{
|
||||
fs.SetLength(0L);
|
||||
await nfs.CopyToAsync(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File.Move(tempDownloadingPath, tempPath, true);
|
||||
return tempPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (File.Exists(tempPath)) File.Delete(tempPath);
|
||||
if (File.Exists(tempDownloadingPath)) File.Delete(tempDownloadingPath);
|
||||
|
||||
ModBase.Log(ex, $"Try to get online image fail (url = {url}, dest = {tempPath})");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#region 公开属性
|
||||
|
||||
public TimeSpan fileCacheExpiredTime = TimeSpan.FromDays(14d);
|
||||
|
||||
public bool EnableCache
|
||||
{
|
||||
get => (bool)GetValue(EnableCacheProperty);
|
||||
set => SetValue(EnableCacheProperty, value);
|
||||
}
|
||||
|
||||
public new static readonly DependencyProperty EnableCacheProperty =
|
||||
DependencyProperty.Register("EnableCache", typeof(bool), typeof(MyImage), new PropertyMetadata(true));
|
||||
|
||||
/// <summary>
|
||||
/// 与 Image 的 Source 类似。
|
||||
/// 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。
|
||||
/// 支持 WebP 格式的图片。
|
||||
/// </summary>
|
||||
public new string Source // 覆写 Image 的 Source 属性
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
value = null;
|
||||
if ((field ?? "") == (value ?? ""))
|
||||
return;
|
||||
field = value;
|
||||
if (!IsInitialized)
|
||||
return; // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
|
||||
Load();
|
||||
}
|
||||
} = "";
|
||||
|
||||
public new static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string),
|
||||
typeof(MyImage), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyImage)sender).Source = e.NewValue.ToString();
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// 当 Source 首次下载失败时,会从该备用地址加载图片。
|
||||
/// </summary>
|
||||
public string FallbackSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 正在下载网络图片时显示的本地图片。
|
||||
/// </summary>
|
||||
public string LoadingSource { get; set; } = "pack://application:,,,/images/Icons/NoIcon.png";
|
||||
public CornerRadius CornerRadius
|
||||
{
|
||||
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
private static readonly DependencyProperty CornerRadiusProperty =
|
||||
DependencyProperty.Register(
|
||||
"CornerRadius",
|
||||
typeof(CornerRadius),
|
||||
typeof(MyImage),
|
||||
new FrameworkPropertyMetadata(
|
||||
new CornerRadius(-1),
|
||||
OnCornerRadiusChanged)
|
||||
);
|
||||
|
||||
private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((MyImage)d).UpdateClip();
|
||||
}
|
||||
|
||||
private void UpdateClip() // Handles Me.SizeChanged will be added separately
|
||||
{
|
||||
if (ActualWidth > 0 && ActualHeight > 0 &&
|
||||
CornerRadius.TopLeft >= 0 && CornerRadius.TopRight >= 0)
|
||||
{
|
||||
Clip = new RectangleGeometry(
|
||||
new Rect(0, 0, ActualWidth, ActualHeight),
|
||||
CornerRadius.TopLeft,
|
||||
CornerRadius.TopRight);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Grid x:Class="PCL.MyListItem"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Name="PanBack"
|
||||
RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}" Height="42">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</Grid.RenderTransform>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="ColumnCheck" Width="2" />
|
||||
<ColumnDefinition x:Name="ColumnPaddingLeft" Width="0" />
|
||||
<ColumnDefinition x:Name="ColumnLogo" Width="4" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition x:Name="ColumnPaddingRight" Width="4" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="1*" />
|
||||
</Grid.RowDefinitions>
|
||||
<!--<Border x:Name="RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True" IsHitTestVisible="False" Opacity="0" Grid.ColumnSpan="4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}" BorderThickness="1" />-->
|
||||
<!--<StackPanel VerticalAlignment="Center">-->
|
||||
<TextBlock Grid.Row="1" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left"
|
||||
x:Name="LabTitle" IsHitTestVisible="False" Grid.Column="3" Grid.ColumnSpan="2"
|
||||
Text="{Binding Title, ElementName=PanBack}" TextTrimming="CharacterEllipsis"
|
||||
FontSize="{Binding FontSize, ElementName=PanBack}"
|
||||
Foreground="{Binding Foreground, ElementName=PanBack}" Margin="4,0,0,0" />
|
||||
<!--<StackPanel Orientation="Horizontal" x:Name="PanInfo" Grid.Row="2" Grid.Column="3" Visibility="Collapsed" IsHitTestVisible="False">
|
||||
<StackPanel x:Name="PanTags" Orientation="Horizontal" VerticalAlignment="Bottom" Margin="0,0,-3,0">
|
||||
<Border Background="{DynamicResource ColorBrush6}" Padding="3,1" CornerRadius="3" Margin="0,0,4,0" SnapsToDevicePixels="True" UseLayoutRounding="False">
|
||||
<TextBlock Text="科技" Foreground="{DynamicResource ColorBrush2}" FontSize="11" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<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" />
|
||||
</StackPanel>-->
|
||||
<!--</StackPanel>-->
|
||||
</Grid>
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<Grid
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d"
|
||||
x:Class="PCL.MyLoading"
|
||||
MinWidth="50" MinHeight="50" Background="{StaticResource ColorBrushTransparent}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="1*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid UseLayoutRounding="False" SnapsToDevicePixels="False" Width="60" Height="47" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top" Grid.Row="1">
|
||||
<Path Name="PathPickaxe" Stretch="Uniform" Height="35" Margin="10,6,0,0" StrokeThickness="2"
|
||||
Data="M 963.6 858.2 410.816 305.504 C 508.116 213.304 609.204 196.8 711.104 128.6 837.11367 49.573762 879.34045 50.334062 751.5 49.5 611.3 52 471.8 96.2 353.3 182.4 309.8 155.7 252.1 161.2 214.5 198.9 176.9 236.6 171.3 294.2 198 337.7 111.8 456.3 67.6 595.8 65.1 735.9 63.315254 883.82034 65.077966 837.29308 144.2 695.488 212.4 593.588 228.888 492.4 321.088 395.2 L 873.9 948 c 0.60001 0.59999 1.6 0.6 2.2 0 l 87.5 -87.5 c 0.6 -0.7 0.6 -1.6 0 -2.3 z"
|
||||
Stroke="{Binding Foreground, ElementName=PanBack}" HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="55" CenterX="30" CenterY="30" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path Opacity="0" Name="PathLeft" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}" Width="3"
|
||||
Height="5" Data="M0,0L2,0L1,4" Margin="7,41,0,0" RenderTransformOrigin="0.5,0.5"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="-45" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path Opacity="0" Name="PathRight" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}" Width="3"
|
||||
Height="5" Data="M0,0L2,0L1,4" Margin="14,41,0,0" RenderTransformOrigin="0.5,0.5"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="45" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path Opacity="0" Name="PathError" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}"
|
||||
Width="17" Height="17" Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z"
|
||||
Margin="4,23,0,0" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||
<Path.RenderTransform>
|
||||
<ScaleTransform ScaleX="0.6" ScaleY="0.6" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Rectangle Width="25" Height="2" Fill="{Binding Foreground, ElementName=PanBack}" VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="LabText" Text="{DynamicResource Application.Control.Loading.Default}"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap" VerticalAlignment="Top"
|
||||
Foreground="{Binding Foreground, ElementName=PanBack}" FontSize="16" Margin="0,10,0,0" Grid.Row="2" />
|
||||
</Grid>
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using static PCL.MyLoading;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyLoading
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
|
||||
|
||||
public delegate void IsErrorChangedEventHandler(object sender, bool isError);
|
||||
|
||||
public delegate void StateChangedEventHandler(object sender, MyLoadingState newState, MyLoadingState oldState);
|
||||
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public bool AutoRun { get; set; } = true;
|
||||
|
||||
public event IsErrorChangedEventHandler? IsErrorChanged;
|
||||
public event StateChangedEventHandler? StateChanged;
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
#region 颜色
|
||||
|
||||
public SolidColorBrush Foreground
|
||||
{
|
||||
get => (SolidColorBrush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ForegroundProperty =
|
||||
DependencyProperty.Register("Foreground", typeof(SolidColorBrush), typeof(MyLoading));
|
||||
|
||||
public MyLoading()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetCurrentValue(TextProperty, Lang.Text("Application.Control.Loading.Default"));
|
||||
SetCurrentValue(TextErrorProperty, Lang.Text("Application.Control.Loading.Failed"));
|
||||
SetResourceReference(ForegroundProperty, "ColorBrush3");
|
||||
IsErrorChanged += (_, _) => RefreshText();
|
||||
Loaded += (_, _) => RefreshText();
|
||||
Loaded += (_, _) => InitState();
|
||||
Loaded += (_, _) => RefreshState();
|
||||
Unloaded += (_, _) => RefreshState();
|
||||
MouseLeftButtonUp += Button_MouseUp;
|
||||
MouseLeftButtonDown += Button_MouseDown;
|
||||
MouseLeave += Button_MouseLeave;
|
||||
MouseLeftButtonUp += Button_MouseLeave;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文本
|
||||
|
||||
private bool _ShowProgress { get; set; }
|
||||
|
||||
public bool ShowProgress
|
||||
{
|
||||
get => _ShowProgress;
|
||||
set
|
||||
{
|
||||
if (_ShowProgress == value)
|
||||
return;
|
||||
_ShowProgress = value;
|
||||
RefreshText();
|
||||
}
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register("Text", typeof(string), typeof(MyLoading),
|
||||
new PropertyMetadata("", (d, e) => ((MyLoading)d).RefreshText()));
|
||||
|
||||
public string TextError
|
||||
{
|
||||
get => (string)GetValue(TextErrorProperty);
|
||||
set => SetValue(TextErrorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextErrorProperty =
|
||||
DependencyProperty.Register("TextError", typeof(string), typeof(MyLoading),
|
||||
new PropertyMetadata("", (d, e) => ((MyLoading)d).RefreshText()));
|
||||
|
||||
/// <summary>
|
||||
/// 是否在使用 Loader 时使用 Loader 的错误输出来替换默认的错误文本显示。
|
||||
/// </summary>
|
||||
public bool TextErrorInherit { get; set; } = true;
|
||||
|
||||
private void RefreshText()
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (InnerState == MyLoadingState.Error)
|
||||
{
|
||||
if (TextErrorInherit && State.IsLoader)
|
||||
{
|
||||
var ex = State.Error;
|
||||
if (ex is null)
|
||||
{
|
||||
LabText.Text = Lang.Text("Application.Control.Loading.UnknownError");
|
||||
}
|
||||
else
|
||||
{
|
||||
while (ex.InnerException is not null) ex = ex.InnerException;
|
||||
LabText.Text = ModBase.StrTrim(ex.Message).ToString();
|
||||
if (new[]
|
||||
{
|
||||
"远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称", "由于目标计算机积极拒绝", "操作已超时", "操作超时", "服务器超时", "连接超时"
|
||||
}.Any(s => LabText.Text.Contains(s)))
|
||||
LabText.Text = Lang.Text("Application.Control.Loading.NetworkUnstable");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LabText.Text = TextError;
|
||||
}
|
||||
}
|
||||
else if (ShowProgress && State.IsLoader)
|
||||
{
|
||||
LabText.Text = Text + " - " + Lang.Number(State.Progress, "P0");
|
||||
}
|
||||
else
|
||||
{
|
||||
LabText.Text = Text;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态改变
|
||||
|
||||
// 状态枚举
|
||||
public enum MyLoadingState
|
||||
{
|
||||
Unloaded = -1,
|
||||
Run = 0,
|
||||
Stop = 1,
|
||||
Error = 2
|
||||
}
|
||||
|
||||
// 用于外部改变的公开状态
|
||||
private ILoadingTrigger _State
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.Synchronized)]
|
||||
get => field;
|
||||
|
||||
[MethodImpl(MethodImplOptions.Synchronized)]
|
||||
set
|
||||
{
|
||||
if (field is not null)
|
||||
{
|
||||
field.ProgressChanged -= (_, _) => RefreshText();
|
||||
field.LoadingStateChanged -= (_, _) => RefreshState();
|
||||
}
|
||||
|
||||
field = value;
|
||||
if (field is not null)
|
||||
{
|
||||
field.ProgressChanged += (_, _) => RefreshText();
|
||||
field.LoadingStateChanged += (_, _) => RefreshState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ILoadingTrigger State
|
||||
{
|
||||
get
|
||||
{
|
||||
InitState();
|
||||
return _State;
|
||||
}
|
||||
set
|
||||
{
|
||||
_State = value;
|
||||
RefreshState();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitState()
|
||||
{
|
||||
if (_State is null)
|
||||
{
|
||||
_State = new MyLoadingStateSimulator();
|
||||
if (AutoRun)
|
||||
_State.LoadingState = MyLoadingState.Run;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshState()
|
||||
{
|
||||
if (_State.LoadingState == MyLoadingState.Run && !IsLoaded)
|
||||
InnerState = MyLoadingState.Stop;
|
||||
InnerState = _State.LoadingState;
|
||||
OuterState = _State.LoadingState;
|
||||
AniLoop();
|
||||
}
|
||||
|
||||
// 用于引发外部事件的状态
|
||||
private MyLoadingState _OuterState { get; set; } = MyLoadingState.Unloaded;
|
||||
|
||||
private MyLoadingState OuterState
|
||||
{
|
||||
get => _OuterState;
|
||||
set
|
||||
{
|
||||
if (_OuterState == value)
|
||||
return;
|
||||
var oldValue = _OuterState;
|
||||
_OuterState = value;
|
||||
// 引发事件
|
||||
StateChanged?.Invoke(this, value, oldValue);
|
||||
if (oldValue == MyLoadingState.Error != (value == MyLoadingState.Error))
|
||||
IsErrorChanged?.Invoke(this, value == MyLoadingState.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 用于引发内部动画事件的状态
|
||||
private MyLoadingState _InnerState { get; set; } = MyLoadingState.Unloaded;
|
||||
|
||||
private MyLoadingState InnerState
|
||||
{
|
||||
get => _InnerState;
|
||||
set
|
||||
{
|
||||
if (_InnerState == value)
|
||||
return;
|
||||
var oldValue = _InnerState;
|
||||
_InnerState = value;
|
||||
// 引发事件
|
||||
AniLoop();
|
||||
if (oldValue == MyLoadingState.Error != (value == MyLoadingState.Error))
|
||||
ErrorAnimation(this, value == MyLoadingState.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 动画
|
||||
|
||||
/// <summary>
|
||||
/// 是否需要动画。
|
||||
/// </summary>
|
||||
public bool HasAnimation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 主动画循环是否正在运行中。
|
||||
/// </summary>
|
||||
private bool isLooping;
|
||||
|
||||
private void AniLoop()
|
||||
{
|
||||
// 这坨循环代码也是老屎坑了,救救.jpg
|
||||
if (!HasAnimation || isLooping || !(InnerState == MyLoadingState.Run) || ModAnimation.aniSpeed > 10d ||
|
||||
!IsLoaded)
|
||||
return;
|
||||
isLooping = true;
|
||||
errorAnimationWaiting = true;
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaRotateTransform(PathPickaxe, -20 - ((RotateTransform)PathPickaxe.RenderTransform).Angle, 350,
|
||||
250, new ModAnimation.AniEaseInBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaRotateTransform(PathPickaxe, 50d, 900, ease: new ModAnimation.AniEaseOutFluent(),
|
||||
after: true),
|
||||
ModAnimation.AaRotateTransform(PathPickaxe, 25d, 900,
|
||||
ease: new ModAnimation.AniEaseOutElastic(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
PathLeft.Opacity = 1d;
|
||||
PathLeft.Margin = new Thickness(7d, 41d, 0d, 0d);
|
||||
PathRight.Opacity = 1d;
|
||||
PathRight.Margin = new Thickness(14d, 41d, 0d, 0d);
|
||||
errorAnimationWaiting = false;
|
||||
}),
|
||||
ModAnimation.AaOpacity(PathLeft, -1, 100, 50),
|
||||
ModAnimation.AaX(PathLeft, -5, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaY(PathLeft, -6, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaOpacity(PathRight, -1, 100, 50),
|
||||
ModAnimation.AaX(PathRight, 5d, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaY(PathRight, -6, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
isLooping = false;
|
||||
AniLoop();
|
||||
}, after: true)
|
||||
}, "MyLoader Loop " + uuid + "/" + ModBase.GetUuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 镐子是否还没挥下去,要求错误动画等待。
|
||||
/// </summary>
|
||||
private bool errorAnimationWaiting;
|
||||
|
||||
private void ErrorAnimation(object sender, bool isError)
|
||||
{
|
||||
if (isError)
|
||||
{
|
||||
// 非错误变为错误
|
||||
var wait = errorAnimationWaiting ? 400 : 0;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrushRedLight", 300),
|
||||
ModAnimation.AaOpacity(PathError, 1d - PathError.Opacity, 100, 300 + wait),
|
||||
ModAnimation.AaScaleTransform(PathError, 1d - ((ScaleTransform)PathError.RenderTransform).ScaleX,
|
||||
400, 300 + wait, new ModAnimation.AniEaseOutBack())
|
||||
}, "MyLoader Error " + uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 错误变为非错误
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PathError, -PathError.Opacity, 100),
|
||||
ModAnimation.AaScaleTransform(PathError, 0.5d - ((ScaleTransform)PathError.RenderTransform).ScaleX,
|
||||
200),
|
||||
ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrush3", 300)
|
||||
}, "MyLoader Error " + uuid);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 点击事件
|
||||
|
||||
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Click?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private bool isMouseDown;
|
||||
|
||||
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
|
||||
isMouseDown = true;
|
||||
}
|
||||
|
||||
private void Button_MouseLeave(object sender, object e)
|
||||
{
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public interface ILoadingTrigger
|
||||
{
|
||||
delegate void LoadingStateChangedEventHandler(MyLoadingState newState, MyLoadingState oldState);
|
||||
|
||||
delegate void ProgressChangedEventHandler(double newProgress, double oldProgress);
|
||||
|
||||
bool IsLoader { get; }
|
||||
|
||||
double Progress { get; }
|
||||
Exception? Error { get; }
|
||||
|
||||
MyLoadingState LoadingState { get; set; }
|
||||
event LoadingStateChangedEventHandler? LoadingStateChanged;
|
||||
event ProgressChangedEventHandler? ProgressChanged;
|
||||
}
|
||||
|
||||
public class MyLoadingStateSimulator : ILoadingTrigger
|
||||
{
|
||||
private MyLoadingState _LoadingState { get; set; } = MyLoadingState.Unloaded;
|
||||
|
||||
public MyLoadingState LoadingState
|
||||
{
|
||||
get => _LoadingState;
|
||||
set
|
||||
{
|
||||
if (_LoadingState == value)
|
||||
return;
|
||||
var oldState = _LoadingState;
|
||||
_LoadingState = value;
|
||||
LoadingStateChanged?.Invoke(value, oldState);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoader { get; } = false;
|
||||
|
||||
public double Progress => 0;
|
||||
public Exception? Error => null;
|
||||
|
||||
public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged;
|
||||
public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.UI.Controls.SvgIcon;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyMenuItem : MenuItem
|
||||
{
|
||||
// 指向动画
|
||||
|
||||
private const int AnimationTimeIn = 100;
|
||||
private const int AnimationTimeOut = 200;
|
||||
|
||||
public static readonly DependencyProperty SvgIconProperty = DependencyProperty.Register(
|
||||
nameof(SvgIcon),
|
||||
typeof(string),
|
||||
typeof(MyMenuItem),
|
||||
new PropertyMetadata(string.Empty, OnSvgIconChanged));
|
||||
|
||||
private SvgIcon? _svgIconControl;
|
||||
private string _colorName;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyMenuItem()
|
||||
{
|
||||
Loaded += MyMenuItem_Loaded;
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
}
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get => (string)GetValue(SvgIconProperty);
|
||||
set => SetValue(SvgIconProperty, value);
|
||||
}
|
||||
|
||||
private static void OnSvgIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is MyMenuItem { IsLoaded: true } item)
|
||||
item.UpdateTemplateIcon();
|
||||
}
|
||||
|
||||
private (string BackName, string ForeName, int Time) GetVisualState()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return ("ColorBrushTransparent", "ColorBrushGray5", AnimationTimeOut);
|
||||
if (IsMouseOver)
|
||||
return ("ColorBrush6", "ColorBrush2", AnimationTimeIn);
|
||||
return ("ColorBrushTransparent", "ColorBrush1", AnimationTimeOut);
|
||||
}
|
||||
|
||||
private void MyMenuItem_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateTemplateIcon();
|
||||
((ContextMenu)Parent).Opacity = Config.Preference.Theme.WindowOpacity / 1000.0 + 0.4;
|
||||
}
|
||||
|
||||
private void UpdateTemplateIcon()
|
||||
{
|
||||
var iconControl = (Path)GetTemplateChild("Icon");
|
||||
if (iconControl is null)
|
||||
return;
|
||||
|
||||
if (SvgIconControlHelper.HasSvgIcon(SvgIcon))
|
||||
{
|
||||
iconControl.Visibility = Visibility.Collapsed;
|
||||
EnsureSvgIconControl(iconControl);
|
||||
_svgIconControl!.Icon = SvgIcon;
|
||||
_svgIconControl.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
_svgIconControl?.Visibility = Visibility.Collapsed;
|
||||
if (Icon is null) return;
|
||||
|
||||
iconControl.Visibility = Visibility.Visible;
|
||||
iconControl.Data = (Geometry)new GeometryConverter().ConvertFromString(Icon.ToString());
|
||||
}
|
||||
|
||||
private void EnsureSvgIconControl(Path iconControl)
|
||||
{
|
||||
if (_svgIconControl is not null)
|
||||
return;
|
||||
|
||||
_svgIconControl = new SvgIcon
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Stretch = Stretch.Uniform,
|
||||
Margin = iconControl.Margin,
|
||||
Height = iconControl.Height,
|
||||
Width = iconControl.Width,
|
||||
IsHitTestVisible = false,
|
||||
Visibility = Visibility.Collapsed
|
||||
};
|
||||
_svgIconControl.SetBinding(Core.UI.Controls.SvgIcon.SvgIcon.IconBrushProperty,
|
||||
new Binding(nameof(Foreground)) { Source = this });
|
||||
|
||||
if (VisualTreeHelper.GetParent(iconControl) is not Grid grid) return;
|
||||
|
||||
Grid.SetColumn(_svgIconControl, Grid.GetColumn(iconControl));
|
||||
Grid.SetRow(_svgIconControl, Grid.GetRow(iconControl));
|
||||
grid.Children.Add(_svgIconControl);
|
||||
}
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
var (backName, foreName, time) = GetVisualState();
|
||||
|
||||
// 重复性验证
|
||||
if ((_colorName ?? "") == (backName ?? ""))
|
||||
return;
|
||||
_colorName = backName;
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, BackgroundProperty, backName, time),
|
||||
ModAnimation.AaColor(this, ForegroundProperty, foreName, time)
|
||||
}, "MyMenuItem Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyMenuItem Color " + Uuid);
|
||||
SetResourceReference(BackgroundProperty, backName);
|
||||
SetResourceReference(ForegroundProperty, foreName);
|
||||
}
|
||||
}
|
||||
|
||||
private void MyMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Grid x:Class="PCL.MyMsgInput"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:my="clr-namespace:PCL"
|
||||
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Border Name="PanBorder" CornerRadius="7" Background="{DynamicResource ColorBrushBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="2"
|
||||
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
|
||||
</Border.Effect>
|
||||
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
<RowDefinition Height="13" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
|
||||
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" Text="测试标题文本"
|
||||
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
|
||||
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
|
||||
<my:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanText" Margin="0,0,0,7"
|
||||
Padding="7,0,15,0"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
DeltaMult="0.7">
|
||||
<TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabText" FontSize="15"
|
||||
LineHeight="18"
|
||||
Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" Padding="1" />
|
||||
</my:MyScrollViewer>
|
||||
<my:MyTextBox Grid.Row="4" Margin="7,0,7,12" x:Name="TextArea" UseLayoutRounding="False" MaxLength="1000"
|
||||
MinWidth="450" />
|
||||
<StackPanel Grid.Row="5" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
|
||||
Margin="150,0,8,0" Orientation="Horizontal">
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮1" x:Name="Btn1" Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
|
||||
Click="Btn1_Click" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮2" x:Name="Btn2" Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
|
||||
Click="Btn2_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyMsgInput
|
||||
{
|
||||
private readonly ModMain.MyMsgBoxConverter myConverter;
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public MyMsgInput(ModMain.MyMsgBoxConverter converter)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
AppendUniqueNameSuffix(Btn1);
|
||||
AppendUniqueNameSuffix(Btn2);
|
||||
myConverter = converter;
|
||||
LabTitle.Text = converter.Title;
|
||||
LabText.Text = converter.Text;
|
||||
PanText.Visibility = string.IsNullOrEmpty(converter.Text) ? Visibility.Collapsed : Visibility.Visible;
|
||||
TextArea.Text = (string)converter.Content;
|
||||
TextArea.HintText = converter.HintText;
|
||||
TextArea.ValidateRules = converter.ValidateRules;
|
||||
ConfigurePrimaryButton(converter.Button1, converter.IsWarn);
|
||||
ConfigureSecondaryButton(converter.Button2);
|
||||
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"输入弹窗初始化失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
Loaded += Load;
|
||||
}
|
||||
|
||||
private void AppendUniqueNameSuffix(FrameworkElement element)
|
||||
{
|
||||
element.Name += ModBase.GetUuid();
|
||||
}
|
||||
|
||||
private void ConfigurePrimaryButton(string text, bool isWarn)
|
||||
{
|
||||
Btn1.Text = text;
|
||||
if (isWarn)
|
||||
{
|
||||
Btn1.ColorType = MyButton.ColorState.Red;
|
||||
LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureSecondaryButton(string text)
|
||||
{
|
||||
Btn2.Text = text;
|
||||
Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void Load(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UI 初始化
|
||||
if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
|
||||
Btn1.ColorType = MyButton.ColorState.Highlight;
|
||||
TextArea.Focus();
|
||||
TextArea.SelectionStart = TextArea.Text.Length;
|
||||
// 动画
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
|
||||
(myConverter.IsWarn
|
||||
? new ModBase.MyColor(140d, 80d, 0d, 0d)
|
||||
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
|
||||
"PanMsgBackground Background");
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d, 120, 60),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
|
||||
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 300, 60,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyMsgBox " + uuid);
|
||||
// 记录日志
|
||||
ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"输入弹窗加载失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
// 结束线程阻塞
|
||||
myConverter.WaitFrame.Continue = false;
|
||||
ComponentDispatcher.PopModal();
|
||||
// 动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!ModMain.WaitingMyMsgBox.Any())
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
|
||||
BlurBorder.BackgroundProperty,
|
||||
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
}, 30),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
|
||||
150, 0, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
|
||||
}, "MyMsgBox " + uuid);
|
||||
}
|
||||
|
||||
public void Btn1_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
TextArea.Validate(); // #5773
|
||||
if (myConverter.IsExited || !TextArea.IsValidated)
|
||||
return;
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = TextArea.Text;
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Btn2_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = null;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void TextCaption_ValidateChanged(object sender, EventArgs e)
|
||||
{
|
||||
Btn1.IsEnabled = TextArea.IsValidated;
|
||||
}
|
||||
|
||||
private void Drag(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
if (e.GetPosition(ShapeLine).Y <= 2d)
|
||||
ModMain.frmMain.DragMove();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"拖拽移动失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Grid x:Class="PCL.MyMsgMarkdown"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:my="clr-namespace:PCL"
|
||||
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
|
||||
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Border Name="PanBorder" CornerRadius="7" Background="{DynamicResource ColorBrushBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="2"
|
||||
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
|
||||
</Border.Effect>
|
||||
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
<RowDefinition Height="13" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
|
||||
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" Text="测试标题文本"
|
||||
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
|
||||
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
|
||||
<my:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17"
|
||||
Padding="0,0,7,0"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
DeltaMult="0.7">
|
||||
<markdig:MarkdownViewer VerticalAlignment="Top" Name="LabCaption"
|
||||
Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" />
|
||||
</my:MyScrollViewer>
|
||||
<StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
|
||||
Margin="150,0,8,0" Orientation="Horizontal">
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮1" x:Name="Btn1" Margin="12,0,0,0"
|
||||
Click="Btn1_Click" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
|
||||
UseLayoutRounding="False" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮2" x:Name="Btn2" Margin="12,0,0,0"
|
||||
Click="Btn2_Click" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
|
||||
UseLayoutRounding="False" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮3" x:Name="Btn3" Margin="12,0,0,0"
|
||||
Click="Btn3_Click" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
|
||||
UseLayoutRounding="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyMsgMarkdown
|
||||
{
|
||||
private readonly ModMain.MyMsgBoxConverter myConverter;
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public MyMsgMarkdown(ModMain.MyMsgBoxConverter converter)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
AppendUniqueNameSuffix(Btn1);
|
||||
AppendUniqueNameSuffix(Btn2);
|
||||
AppendUniqueNameSuffix(Btn3);
|
||||
myConverter = converter;
|
||||
LabTitle.Text = converter.Title;
|
||||
LabCaption.Markdown = converter.Text;
|
||||
DataContext = this;
|
||||
ConfigurePrimaryButton(converter.Button1, converter.IsWarn);
|
||||
ConfigureSecondaryButton(Btn2, converter.Button2);
|
||||
ConfigureSecondaryButton(Btn3, converter.Button3);
|
||||
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"普通弹窗初始化失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
Loaded += Load;
|
||||
}
|
||||
|
||||
private void AppendUniqueNameSuffix(FrameworkElement element)
|
||||
{
|
||||
element.Name += ModBase.GetUuid();
|
||||
}
|
||||
|
||||
private void ConfigurePrimaryButton(string text, bool isWarn)
|
||||
{
|
||||
Btn1.Text = text;
|
||||
if (isWarn)
|
||||
{
|
||||
Btn1.ColorType = MyButton.ColorState.Red;
|
||||
LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureSecondaryButton(MyButton button, string text)
|
||||
{
|
||||
button.Text = text;
|
||||
button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void Load(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UI 初始化
|
||||
if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
|
||||
Btn1.ColorType = MyButton.ColorState.Highlight;
|
||||
Btn1.Focus();
|
||||
// 动画
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
|
||||
(myConverter.IsWarn
|
||||
? new ModBase.MyColor(140d, 80d, 0d, 0d)
|
||||
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
|
||||
"PanMsgBackground Background");
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d, 120, 60),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
|
||||
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 300, 60,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyMsgBox " + uuid);
|
||||
// 记录日志
|
||||
ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Markdown);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"普通弹窗加载失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
// 结束线程阻塞
|
||||
if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2))
|
||||
myConverter.WaitFrame.Continue = false;
|
||||
ComponentDispatcher.PopModal();
|
||||
// 动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!ModMain.WaitingMyMsgBox.Any())
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
|
||||
BlurBorder.BackgroundProperty,
|
||||
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
}, 30),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
|
||||
150, 0, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
|
||||
}, "MyMsgBox " + uuid);
|
||||
}
|
||||
|
||||
public void Btn1_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button1Action is not null)
|
||||
{
|
||||
myConverter.Button1Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 1;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Btn2_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button2Action is not null)
|
||||
{
|
||||
myConverter.Button2Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 2;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Btn3_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button3Action is not null)
|
||||
{
|
||||
myConverter.Button3Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 3;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void Drag(object? sender = null, MouseButtonEventArgs? e = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
if (e.GetPosition(ShapeLine).Y <= 2d)
|
||||
ModMain.frmMain.DragMove();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"拖拽移动失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Grid x:Class="PCL.MyMsgSelect"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:my="clr-namespace:PCL"
|
||||
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Border Name="PanBorder" CornerRadius="7" Background="{DynamicResource ColorBrushBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="2"
|
||||
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
|
||||
</Border.Effect>
|
||||
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
<RowDefinition Height="13" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
|
||||
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" Text="测试标题文本"
|
||||
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
|
||||
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
|
||||
<my:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17"
|
||||
Padding="7,0,15,0"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
DeltaMult="0.7">
|
||||
<StackPanel x:Name="PanSelection" Orientation="Vertical" />
|
||||
</my:MyScrollViewer>
|
||||
<StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
|
||||
Margin="150,0,8,0" Orientation="Horizontal">
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮1" x:Name="Btn1" Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮2" x:Name="Btn2" Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,205 @@
|
||||
using System.Collections;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyMsgSelect
|
||||
{
|
||||
private readonly ModMain.MyMsgBoxConverter myConverter;
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
private int selectedIndex = -1;
|
||||
|
||||
public MyMsgSelect(ModMain.MyMsgBoxConverter converter)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
AppendUniqueNameSuffix(Btn1);
|
||||
AppendUniqueNameSuffix(Btn2);
|
||||
myConverter = converter;
|
||||
LabTitle.Text = converter.Title;
|
||||
ConfigurePrimaryButton(converter.Button1, converter.IsWarn);
|
||||
ConfigureSecondaryButton(converter.Button2);
|
||||
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
|
||||
InitializeSelectionList(converter.Content);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"选择弹窗初始化失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
Loaded += Load;
|
||||
Btn1.Click += Btn1_Click;
|
||||
Btn2.Click += Btn2_Click;
|
||||
LabTitle.MouseLeftButtonDown += Drag;
|
||||
PanBorder.MouseLeftButtonDown += Drag;
|
||||
}
|
||||
|
||||
private void AppendUniqueNameSuffix(FrameworkElement element)
|
||||
{
|
||||
element.Name += ModBase.GetUuid();
|
||||
}
|
||||
|
||||
private void ConfigurePrimaryButton(string text, bool isWarn)
|
||||
{
|
||||
Btn1.Text = text;
|
||||
if (isWarn)
|
||||
{
|
||||
Btn1.ColorType = MyButton.ColorState.Red;
|
||||
LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureSecondaryButton(string text)
|
||||
{
|
||||
Btn2.Text = text;
|
||||
Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void InitializeSelectionList(object content)
|
||||
{
|
||||
// 添加选择控件
|
||||
Btn1.IsEnabled = false;
|
||||
foreach (var rawContent in (IEnumerable)content)
|
||||
{
|
||||
// 1. Initialize and get the actual element
|
||||
// Note: We use a new variable because 'foreach' variables are read-only
|
||||
var selectionContent = MyVirtualizingElement.TryInit((FrameworkElement)rawContent);
|
||||
|
||||
// 2. Interface casting and event subscription
|
||||
if (selectionContent is IMyRadio selection)
|
||||
{
|
||||
PanSelection.Children.Add((UIElement)selection);
|
||||
selection.Check += (sender, e) => OnChecked((IMyRadio)sender, e);
|
||||
|
||||
// 3. Property configuration based on specific type
|
||||
if (selection is MyListItem listItem)
|
||||
{
|
||||
listItem.Type = MyListItem.CheckType.RadioBox;
|
||||
listItem.MinHeight = 24.0;
|
||||
}
|
||||
else if (selection is MyRadioBox radioBox)
|
||||
{
|
||||
radioBox.MinHeight = 24.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Load(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UI 初始化
|
||||
if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
|
||||
Btn1.ColorType = MyButton.ColorState.Highlight;
|
||||
// 动画
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
|
||||
(myConverter.IsWarn
|
||||
? new ModBase.MyColor(140d, 80d, 0d, 0d)
|
||||
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
|
||||
"PanMsgBackground Background");
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d, 120, 60),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
|
||||
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 300, 60,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyMsgBox " + uuid);
|
||||
// 记录日志
|
||||
ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"选择弹窗加载失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
// 结束线程阻塞
|
||||
myConverter.WaitFrame.Continue = false;
|
||||
ComponentDispatcher.PopModal();
|
||||
// 动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!ModMain.WaitingMyMsgBox.Any())
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
|
||||
BlurBorder.BackgroundProperty,
|
||||
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
}, 30),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
|
||||
150, 0, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
|
||||
}, "MyMsgBox " + uuid);
|
||||
}
|
||||
|
||||
public void Btn1_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited || selectedIndex == -1)
|
||||
return;
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = selectedIndex;
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Btn2_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = null;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnChecked(IMyRadio sender, EventArgs e)
|
||||
{
|
||||
Btn1.IsEnabled = true;
|
||||
selectedIndex = PanSelection.Children.IndexOf((UIElement)sender);
|
||||
}
|
||||
|
||||
private void Drag(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
if (e.GetPosition(ShapeLine).Y <= 2d)
|
||||
ModMain.frmMain.DragMove();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"拖拽移动失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Grid x:Class="PCL.MyMsgText"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:my="clr-namespace:PCL"
|
||||
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Border Name="PanBorder" CornerRadius="7" Background="{DynamicResource ColorBrushBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="2"
|
||||
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
|
||||
</Border.Effect>
|
||||
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
<RowDefinition Height="13" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
|
||||
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" Text="测试标题文本"
|
||||
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
|
||||
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
|
||||
<my:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17"
|
||||
Padding="7,0,15,0"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
DeltaMult="0.7">
|
||||
<TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabCaption"
|
||||
FontSize="15" LineHeight="18"
|
||||
Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" Padding="1" />
|
||||
</my:MyScrollViewer>
|
||||
<StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
|
||||
Margin="150,0,8,0" Orientation="Horizontal">
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮1" Click="Btn1_Click" x:Name="Btn1"
|
||||
Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮2" Click="Btn2_Click" x:Name="Btn2"
|
||||
Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
|
||||
<my:MyButton ColorType="Normal" x:FieldModifier="public" Text="测试按钮3" Click="Btn3_Click" x:Name="Btn3"
|
||||
Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,196 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyMsgText
|
||||
{
|
||||
private readonly ModMain.MyMsgBoxConverter myConverter;
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public MyMsgText(ModMain.MyMsgBoxConverter converter)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
AppendUniqueNameSuffix(Btn1);
|
||||
AppendUniqueNameSuffix(Btn2);
|
||||
AppendUniqueNameSuffix(Btn3);
|
||||
myConverter = converter;
|
||||
LabTitle.Text = converter.Title;
|
||||
LabCaption.Text = converter.Text;
|
||||
ConfigurePrimaryButton(converter.Button1, converter.IsWarn);
|
||||
ConfigureSecondaryButton(Btn2, converter.Button2);
|
||||
ConfigureSecondaryButton(Btn3, converter.Button3);
|
||||
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"普通弹窗初始化失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
Loaded += Load;
|
||||
}
|
||||
|
||||
private void AppendUniqueNameSuffix(FrameworkElement element)
|
||||
{
|
||||
element.Name += ModBase.GetUuid();
|
||||
}
|
||||
|
||||
private void ConfigurePrimaryButton(string text, bool isWarn)
|
||||
{
|
||||
Btn1.Text = text;
|
||||
if (isWarn)
|
||||
{
|
||||
Btn1.ColorType = MyButton.ColorState.Red;
|
||||
LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureSecondaryButton(MyButton button, string text)
|
||||
{
|
||||
button.Text = text;
|
||||
button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void Load(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UI 初始化
|
||||
if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
|
||||
Btn1.ColorType = MyButton.ColorState.Highlight;
|
||||
Btn1.Focus();
|
||||
// 动画
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
|
||||
(myConverter.IsWarn
|
||||
? new ModBase.MyColor(140d, 80d, 0d, 0d)
|
||||
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
|
||||
"PanMsgBackground Background");
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d, 120, 60),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
|
||||
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 300, 60,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyMsgBox " + uuid);
|
||||
// 记录日志
|
||||
ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"普通弹窗加载失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
// 结束线程阻塞
|
||||
if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2))
|
||||
myConverter.WaitFrame.Continue = false;
|
||||
ComponentDispatcher.PopModal();
|
||||
// 动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!ModMain.WaitingMyMsgBox.Any())
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
|
||||
BlurBorder.BackgroundProperty,
|
||||
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
}, 30),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
|
||||
150, 0, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
|
||||
}, "MyMsgBox " + uuid);
|
||||
}
|
||||
|
||||
public void Btn1_Click(object? sender = null, MouseButtonEventArgs? e = null)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button1Action is not null)
|
||||
{
|
||||
myConverter.Button1Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 1;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Btn2_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button2Action is not null)
|
||||
{
|
||||
myConverter.Button2Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 2;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Btn3_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
if (myConverter.Button3Action is not null)
|
||||
{
|
||||
myConverter.Button3Action();
|
||||
}
|
||||
else
|
||||
{
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = 3;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void Drag(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
if (e.GetPosition(ShapeLine).Y <= 2d)
|
||||
ModMain.frmMain.DragMove();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"拖拽移动失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.MessageBox.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyPageLeft : Grid
|
||||
{
|
||||
public static DependencyProperty AnimatedControlProperty =
|
||||
DependencyProperty.Register("AnimatedControl", typeof(FrameworkElement), typeof(MyPageLeft));
|
||||
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
private bool _animatedControlNullWarned;
|
||||
|
||||
// 执行逐个进入动画的控件
|
||||
public FrameworkElement AnimatedControl
|
||||
{
|
||||
get
|
||||
{
|
||||
var res = GetValue(AnimatedControlProperty);
|
||||
if (res is null && !_animatedControlNullWarned)
|
||||
{
|
||||
_animatedControlNullWarned = true;
|
||||
ModBase.Log($"[MyPageLeft] 获取到 AnimatedControl(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug);
|
||||
}
|
||||
|
||||
return (FrameworkElement)res;
|
||||
}
|
||||
set => SetValue(AnimatedControlProperty, value);
|
||||
}
|
||||
|
||||
public void TriggerShowAnimation()
|
||||
{
|
||||
if (AnimatedControl is null)
|
||||
{
|
||||
// 缩放动画
|
||||
if (RenderTransform is not ScaleTransform)
|
||||
{
|
||||
RenderTransform = new ScaleTransform(0.96d, 0.96d);
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d);
|
||||
}
|
||||
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX,
|
||||
ease: new ModAnimation.AniEaseOutBack((ModAnimation.AniEasePower)2)),
|
||||
ModAnimation.AaOpacity(this, 1d, 100)
|
||||
}, "PageLeft PageChange " + uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 逐个进入动画
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
var id = 0;
|
||||
var delay = 0;
|
||||
foreach (var ElementRaw in GetAllAnimControls(true))
|
||||
{
|
||||
var element = MyVirtualizingElement.TryInit(ElementRaw);
|
||||
if (element.Visibility == Visibility.Collapsed)
|
||||
{
|
||||
// 还原之前的隐藏动画可能导致的改变(#2436)
|
||||
element.Opacity = 1d;
|
||||
element.RenderTransform = new TranslateTransform(0d, 0d);
|
||||
if (element is MyListItem)
|
||||
((MyListItem)element).isMouseOverAnimationEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
element.Opacity = 0d;
|
||||
element.RenderTransform = new TranslateTransform(-25, 0d);
|
||||
if (element is MyListItem)
|
||||
((MyListItem)element).isMouseOverAnimationEnabled = false;
|
||||
aniList.Add(ModAnimation.AaOpacity(element, element is TextBlock ? 0.6d : 1d, 100, delay,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
aniList.Add(ModAnimation.AaTranslateX(element, 5d, 200, delay,
|
||||
new ModAnimation.AniEaseOutFluent()));
|
||||
aniList.Add(ModAnimation.AaTranslateX(element, 20d, 300, delay,
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)));
|
||||
if (element is MyListItem)
|
||||
aniList.Add(ModAnimation.AaCode(() =>
|
||||
{
|
||||
((MyListItem)element).isMouseOverAnimationEnabled = true;
|
||||
((MyListItem)element).RefreshColor(this, new EventArgs());
|
||||
}, delay + 280));
|
||||
delay += Math.Max(15 - id, 7) * 2;
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(aniList, "PageLeft PageChange " + uuid);
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerHideAnimation()
|
||||
{
|
||||
if (AnimatedControl is null)
|
||||
{
|
||||
// 缩放动画
|
||||
if (RenderTransform is not ScaleTransform)
|
||||
{
|
||||
RenderTransform = new ScaleTransform(1d, 1d);
|
||||
RenderTransformOrigin = new Point(0.5d, 0.5d);
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScaleTransform(this, 0.95d - ((ScaleTransform)RenderTransform).ScaleX, 110,
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 30)
|
||||
}, "PageLeft PageChange " + uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 逐个退出动画
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
var id = 0;
|
||||
var controls = GetAllAnimControls();
|
||||
foreach (var Element in controls)
|
||||
{
|
||||
aniList.Add(ModAnimation.AaOpacity(Element, -Element.Opacity, 50,
|
||||
(int)Math.Round(70d / controls.Count * id)));
|
||||
aniList.Add(ModAnimation.AaTranslateX(Element, -6, 50, (int)Math.Round(70d / controls.Count * id)));
|
||||
id += 1;
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(aniList, "PageLeft PageChange " + uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历获取所有需要生成动画的控件
|
||||
private List<FrameworkElement> GetAllAnimControls(bool ignoreInvisibility = false)
|
||||
{
|
||||
var allControls = new List<FrameworkElement>();
|
||||
GetAllAnimControls(AnimatedControl, ref allControls, ignoreInvisibility);
|
||||
return allControls;
|
||||
}
|
||||
|
||||
private void GetAllAnimControls(FrameworkElement element, ref List<FrameworkElement> allControls,
|
||||
bool ignoreInvisibility)
|
||||
{
|
||||
if (!ignoreInvisibility && element.Visibility == Visibility.Collapsed)
|
||||
return;
|
||||
if (element is MyTextButton)
|
||||
allControls.Add(element);
|
||||
else if (element is MyListItem)
|
||||
allControls.Add(element);
|
||||
else if (element is ContentControl)
|
||||
GetAllAnimControls((FrameworkElement)((ContentControl)element).Content, ref allControls,
|
||||
ignoreInvisibility);
|
||||
else if (element is Panel)
|
||||
foreach (FrameworkElement Element2 in ((Panel)element).Children)
|
||||
GetAllAnimControls(Element2, ref allControls, ignoreInvisibility);
|
||||
else
|
||||
allControls.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRefreshable
|
||||
{
|
||||
void Refresh();
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using static PCL.ModLoader;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyPageRight : AdornerDecorator
|
||||
{
|
||||
// 当前状态
|
||||
public enum PageStates
|
||||
{
|
||||
Empty, // 默认状态,页面全空
|
||||
LoaderWait, // 加载环初始等待
|
||||
LoaderEnter, // 加载环进入动画
|
||||
LoaderStayForce, // 加载环正常显示(强制等待)
|
||||
LoaderStay, // 加载环正常显示
|
||||
LoaderExit, // 加载环退出动画
|
||||
ContentEnter, // 内容进入动画
|
||||
ContentStay, // 内容正常显示
|
||||
ContentExit, // 刷新导致的全部退出动画,或页面内容退出(子页面更改)导致的全部退出动画
|
||||
PageExit // 切换页面导致的全部退出动画
|
||||
}
|
||||
|
||||
private static readonly DependencyProperty PanScrollProperty =
|
||||
DependencyProperty.Register("PanScroll", typeof(MyScrollViewer), typeof(MyPageRight));
|
||||
|
||||
private bool _panScrollNullWarned;
|
||||
|
||||
public int pageUuid = ModBase.GetUuid();
|
||||
|
||||
// “返回顶部” 按钮检测的滚动区域
|
||||
public MyScrollViewer PanScroll
|
||||
{
|
||||
get
|
||||
{
|
||||
var res = GetValue((DependencyProperty)PanScrollProperty);
|
||||
if (res is null && !_panScrollNullWarned)
|
||||
{
|
||||
_panScrollNullWarned = true;
|
||||
ModBase.Log($"[MyPageRight] 获取到 PanScroll(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug);
|
||||
}
|
||||
|
||||
return (MyScrollViewer)res;
|
||||
}
|
||||
set => SetValue(PanScrollProperty, value);
|
||||
}
|
||||
|
||||
public PageStates PageState
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 页面状态切换为 " + ModBase.GetStringFromEnum(value));
|
||||
}
|
||||
} = PageStates.Empty;
|
||||
|
||||
#region 加载器
|
||||
|
||||
private ModLoader.LoaderBase pageLoader;
|
||||
private Func<object>? pageLoaderInputInvoke;
|
||||
private MyLoading? pageLoaderUi;
|
||||
private FrameworkElement panLoader;
|
||||
private FrameworkElement panContent;
|
||||
private FrameworkElement? panAlways;
|
||||
private bool pageLoaderAutoRun;
|
||||
|
||||
// 初始化
|
||||
/// <summary>
|
||||
/// 表明页面存在需要在后台执行的加载器。
|
||||
/// </summary>
|
||||
/// <param name="loaderUi">MyLoading 控件。</param>
|
||||
/// <param name="panLoader">MyLoading 控件对应的卡片。</param>
|
||||
/// <param name="panContent">加载结束后出现的内容容器。</param>
|
||||
/// <param name="panAlways">无论是否在加载总是要显示的容器。可以为 Nothing。</param>
|
||||
/// <param name="realLoader">在工作线程执行的加载器。</param>
|
||||
/// <param name="finishedInvoke">当加载器执行完成,在 UI 线程触发的 UI 初始化事件。</param>
|
||||
public void PageLoaderInit(MyLoading loaderUi, FrameworkElement panLoader, FrameworkElement panContent,
|
||||
FrameworkElement? panAlways, ModLoader.LoaderBase realLoader, Action<ModLoader.LoaderBase>? finishedInvoke = null,
|
||||
Func<object>? inputInvoke = null, bool autoRun = true)
|
||||
{
|
||||
// 初始化参数
|
||||
this.panLoader = panLoader;
|
||||
this.panContent = panContent;
|
||||
this.panAlways = panAlways;
|
||||
pageLoader = realLoader;
|
||||
pageLoaderUi = loaderUi;
|
||||
pageLoaderInputInvoke = inputInvoke;
|
||||
pageLoaderAutoRun = autoRun;
|
||||
// 添加结束 Invoke
|
||||
if (finishedInvoke is not null)
|
||||
realLoader.PreviewFinish += _ =>
|
||||
{
|
||||
while (PageState == PageStates.PageExit || PageState == PageStates.ContentExit)
|
||||
Thread.Sleep(10); // 不在退出动画时执行 UI 线程操作,避免退出动画被重置
|
||||
ModBase.RunInUiWait(() => finishedInvoke(realLoader));
|
||||
Thread.Sleep(20); // 由于大量初始化控件会导致掉帧,延迟触发 State 改变事件
|
||||
};
|
||||
realLoader.OnStateChangedUi += (loader, newState, oldState) =>
|
||||
ModBase.RunInUi(() => PageLoaderState(loader, newState, oldState));
|
||||
// 隐藏 UI
|
||||
panLoader.Visibility = Visibility.Collapsed;
|
||||
panContent.Visibility = Visibility.Collapsed;
|
||||
panAlways?.Visibility = Visibility.Collapsed;
|
||||
// 初次运行加载器
|
||||
if (pageLoaderAutoRun)
|
||||
{
|
||||
if (pageLoader is ModLoader.LoaderTask task)
|
||||
{
|
||||
task.Start(task.StartGetInputNoType(null, pageLoaderInputInvoke));
|
||||
}
|
||||
else
|
||||
{
|
||||
object? input = null;
|
||||
if (pageLoaderInputInvoke is not null)
|
||||
input = pageLoaderInputInvoke();
|
||||
pageLoader.Start(input);
|
||||
}
|
||||
}
|
||||
|
||||
if (pageLoader.State == ModBase.LoadState.Finished && finishedInvoke is not null)
|
||||
ModBase.RunInUiWait(() => finishedInvoke(realLoader)); // 加载器已提前完成,直接触发事件
|
||||
// 设置加载环
|
||||
pageLoaderUi.State = realLoader;
|
||||
pageLoaderUi.Click += (_, _) =>
|
||||
{
|
||||
if (realLoader.State == ModBase.LoadState.Failed) PageLoaderRestart();
|
||||
}; // 点击重试事件
|
||||
}
|
||||
|
||||
// 重试
|
||||
public void PageLoaderRestart(object input = null, bool isForceRestart = true) // 由外部调用的重试
|
||||
{
|
||||
if (!pageLoaderAutoRun)
|
||||
return;
|
||||
if (pageLoader is LoaderTask task)
|
||||
{
|
||||
pageLoader.Start(task.StartGetInputNoType(input, pageLoaderInputInvoke), isForceRestart);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input is null && pageLoaderInputInvoke is not null)
|
||||
input = pageLoaderInputInvoke;
|
||||
pageLoader.Start(input, isForceRestart);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件
|
||||
|
||||
// 外部触发的事件
|
||||
/// <summary>
|
||||
/// 需要切换到当前页面,并且原本的 Loaded 事件已执行完成。
|
||||
/// 需要根据加载器状态,从 Empty 切换到 ContentEnter、LoaderWait、LoaderEnter。
|
||||
/// </summary>
|
||||
public void PageOnEnter()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnEnter");
|
||||
PageEnter?.Invoke();
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.Empty:
|
||||
{
|
||||
if (pageLoader is null || pageLoader.State == ModBase.LoadState.Finished ||
|
||||
pageLoader.State == ModBase.LoadState.Waiting || pageLoader.State == ModBase.LoadState.Aborted)
|
||||
{
|
||||
// 如果加载器在进入页面时不启动(例如联机),那么在此时就会有 State = Waiting
|
||||
PageState = PageStates.ContentEnter;
|
||||
TriggerEnterAnimation(panAlways, (FrameworkElement)(panContent ?? Child));
|
||||
}
|
||||
else if (pageLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
PageState = PageStates.LoaderWait;
|
||||
ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderWaitFinished, 400),
|
||||
"PageRight PageChange " + pageUuid);
|
||||
}
|
||||
else // PageLoader.State = LoadState.Failed
|
||||
{
|
||||
PageState = PageStates.LoaderEnter;
|
||||
TriggerEnterAnimation(panAlways, panLoader);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PageStates.ContentExit:
|
||||
{
|
||||
// 和上面的一样,但是不管 PanAlways
|
||||
if (pageLoader is null || pageLoader.State == ModBase.LoadState.Finished ||
|
||||
pageLoader.State == ModBase.LoadState.Waiting || pageLoader.State == ModBase.LoadState.Aborted)
|
||||
{
|
||||
PageState = PageStates.ContentEnter;
|
||||
TriggerEnterAnimation((FrameworkElement)(panContent ?? Child));
|
||||
}
|
||||
else if (pageLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
PageState = PageStates.LoaderWait;
|
||||
ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderWaitFinished, 400),
|
||||
"PageRight PageChange " + pageUuid);
|
||||
}
|
||||
else // PageLoader.State = LoadState.Failed
|
||||
{
|
||||
PageState = PageStates.LoaderEnter;
|
||||
TriggerEnterAnimation(panLoader);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PageStates.ContentEnter: // 重复调用 PageOnEnter,直接忽略
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + " 时触发了 PageOnEnter 事件。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event PageEnterEventHandler? PageEnter;
|
||||
|
||||
public delegate void PageEnterEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// 需要切换到其他页面。
|
||||
/// 需要立即切换至 PageExit 或 Empty。
|
||||
/// </summary>
|
||||
public void PageOnExit()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnExit");
|
||||
PageExit?.Invoke();
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.ContentEnter:
|
||||
case PageStates.ContentStay:
|
||||
{
|
||||
PageState = PageStates.PageExit;
|
||||
TriggerExitAnimation(panAlways, (FrameworkElement)(panContent ?? Child));
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderEnter:
|
||||
case PageStates.LoaderStayForce:
|
||||
case PageStates.LoaderStay:
|
||||
{
|
||||
PageState = PageStates.PageExit;
|
||||
TriggerExitAnimation(panAlways, panLoader);
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderWait:
|
||||
{
|
||||
PageState = PageStates.PageExit;
|
||||
TriggerExitAnimation(panAlways);
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderExit:
|
||||
case PageStates.ContentExit:
|
||||
{
|
||||
PageState = PageStates.PageExit;
|
||||
if (panAlways is not null)
|
||||
TriggerExitAnimation(panAlways, (FrameworkElement)(panContent ?? Child));
|
||||
break;
|
||||
}
|
||||
case PageStates.PageExit:
|
||||
case PageStates.Empty:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event PageExitEventHandler? PageExit;
|
||||
|
||||
public delegate void PageExitEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// 即将切换到其他页面,需要强制完成页面状态清理。
|
||||
/// 需要立即切换至 Empty。
|
||||
/// </summary>
|
||||
public void PageOnForceExit()
|
||||
{
|
||||
if (PageState == PageStates.Empty)
|
||||
return;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnForceExit");
|
||||
PageState = PageStates.Empty;
|
||||
ModAnimation.AniStop("PageRight PageChange " + pageUuid);
|
||||
// 由于动画会被强制中止,所以需要手动进行隐藏
|
||||
if (pageLoader is null && Child is not null)
|
||||
{
|
||||
Child.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
panContent.Visibility = Visibility.Collapsed;
|
||||
panLoader.Visibility = Visibility.Collapsed;
|
||||
if (panAlways is not null)
|
||||
panAlways.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PanContent 中的子页面改变,需要让当前内容退出,再显示新的内容。
|
||||
/// 需要在 PageEnter 事件确认要显示的子页面有哪些。
|
||||
/// </summary>
|
||||
public void PageOnContentExit()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnContentExit");
|
||||
if (pageLoader is not null && pageLoader.State == ModBase.LoadState.Loading)
|
||||
throw new Exception("在调用 PageOnContentExit 时,加载器不能为 Loading 状态");
|
||||
// Loading 的加载器可能触发进一步变化,难以预测会触发子页面的动画还是加载器完成的动画
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.ContentEnter:
|
||||
case PageStates.ContentStay:
|
||||
{
|
||||
PageState = PageStates.ContentExit;
|
||||
TriggerExitAnimation(panContent);
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderExit:
|
||||
{
|
||||
PageState = PageStates.ContentExit;
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderEnter:
|
||||
case PageStates.LoaderStayForce:
|
||||
case PageStates.LoaderStay:
|
||||
{
|
||||
PageState = PageStates.ContentExit;
|
||||
TriggerExitAnimation(panLoader);
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderWait:
|
||||
case PageStates.Empty:
|
||||
{
|
||||
PageOnEnter();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内部触发的事件
|
||||
/// <summary>
|
||||
/// 逐个进入动画已执行完成。
|
||||
/// 需要根据目前状态,从 ContentEnter 切换到 ContentStay,或从 LoaderEnter 切换到 LoaderStayForce。
|
||||
/// </summary>
|
||||
private void PageOnEnterAnimationFinished()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnEnterAnimationFinished");
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.ContentEnter:
|
||||
{
|
||||
PageState = PageStates.ContentStay;
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderEnter:
|
||||
{
|
||||
PageState = PageStates.LoaderStayForce;
|
||||
ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderStayFinished, 400),
|
||||
"PageRight PageChange " + pageUuid);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) +
|
||||
" 时触发了 PageOnEnterAnimationFinished 事件。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逐个退出动画已执行完成。
|
||||
/// 需要根据目前状态,从 AllExit 切换到 Empty,或从 LoaderExit 切换到 ContentEnter,或从 ContentExit 重新触发 PageOnEnter。
|
||||
/// </summary>
|
||||
private void PageOnExitAnimationFinished()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnExitAnimationFinished");
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.PageExit:
|
||||
{
|
||||
PageState = PageStates.Empty;
|
||||
break;
|
||||
}
|
||||
case PageStates.ContentExit:
|
||||
{
|
||||
PageOnEnter();
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderExit:
|
||||
{
|
||||
PageState = PageStates.ContentEnter;
|
||||
TriggerEnterAnimation(panContent);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) +
|
||||
" 时触发了 PageOnExitAnimationFinished 事件。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载环进入等待已结束。
|
||||
/// 需要从 LoaderWait 切换到 LoaderEnter。
|
||||
/// </summary>
|
||||
private void PageOnLoaderWaitFinished()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnLoaderWaitFinished");
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.LoaderWait:
|
||||
{
|
||||
PageState = PageStates.LoaderEnter;
|
||||
if (panAlways is not null && panAlways.Visibility == Visibility.Collapsed)
|
||||
TriggerEnterAnimation(panAlways, panLoader);
|
||||
else
|
||||
TriggerEnterAnimation(panLoader);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) +
|
||||
" 时触发了 PageOnLoaderWaitFinished 事件。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载环展示等待已结束。
|
||||
/// 需要从 LoaderStayForce 切换到 LoaderStay 或 LoaderExit。
|
||||
/// </summary>
|
||||
private void PageOnLoaderStayFinished()
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageOnLoaderStayFinished");
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.LoaderStayForce:
|
||||
{
|
||||
if (pageLoader.State == ModBase.LoadState.Finished)
|
||||
{
|
||||
PageState = PageStates.LoaderExit;
|
||||
TriggerExitAnimation(panLoader);
|
||||
}
|
||||
else
|
||||
{
|
||||
PageState = PageStates.LoaderStay;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) +
|
||||
" 时触发了 PageOnLoaderWaitFinished 事件。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局加载状态已改变。
|
||||
/// </summary>
|
||||
private void PageLoaderState(object sender, ModBase.LoadState newState, ModBase.LoadState oldState)
|
||||
{
|
||||
switch (newState)
|
||||
{
|
||||
case ModBase.LoadState.Failed:
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
if (oldState == ModBase.LoadState.Failed || oldState == ModBase.LoadState.Loading)
|
||||
return;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageLoaderState (Start/Refresh)");
|
||||
// (重新)开始运行
|
||||
// 需要从部分状态切换到 ReloadExit
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.ContentEnter:
|
||||
case PageStates.ContentStay:
|
||||
{
|
||||
PageState = PageStates.ContentExit;
|
||||
TriggerExitAnimation(panContent);
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderExit:
|
||||
{
|
||||
PageState = PageStates.ContentExit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Finished:
|
||||
case ModBase.LoadState.Aborted:
|
||||
case ModBase.LoadState.Waiting:
|
||||
{
|
||||
if (oldState != ModBase.LoadState.Failed && oldState != ModBase.LoadState.Loading)
|
||||
return;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[UI] 已触发 PageLoaderState (Stop/Abort)");
|
||||
// 运行结束
|
||||
// 需要从 LoaderWait 切换到 ContentEnter,或从 LoaderStay 切换到 LoaderExit
|
||||
switch (PageState)
|
||||
{
|
||||
case PageStates.LoaderWait:
|
||||
{
|
||||
PageState = PageStates.ContentEnter;
|
||||
if (panAlways is not null && panAlways.Visibility == Visibility.Collapsed)
|
||||
TriggerEnterAnimation(panAlways, panContent);
|
||||
else
|
||||
TriggerEnterAnimation(panContent);
|
||||
|
||||
break;
|
||||
}
|
||||
case PageStates.LoaderStay:
|
||||
{
|
||||
PageState = PageStates.LoaderExit;
|
||||
TriggerExitAnimation(panLoader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 动画
|
||||
|
||||
// 逐个进入动画
|
||||
public void TriggerEnterAnimation(params FrameworkElement[] elements)
|
||||
{
|
||||
var realElements = elements.Where(e => e is not null);
|
||||
foreach (var Element in realElements)
|
||||
Element.Visibility = Visibility.Visible; // 页面均处于默认的隐藏状态
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
var delay = 0;
|
||||
// 基础动画
|
||||
foreach (var Element in realElements)
|
||||
{
|
||||
foreach (var Control in GetAllAnimControls(Element, true))
|
||||
{
|
||||
// 还原被隐藏的卡片的消失动画
|
||||
Control.IsHitTestVisible = true;
|
||||
if (Control.RenderTransform is TranslateTransform)
|
||||
Control.RenderTransform = null;
|
||||
}
|
||||
|
||||
foreach (var Control in GetAllAnimControls(Element))
|
||||
if (Control is MyExtraTextButton)
|
||||
{
|
||||
((MyExtraTextButton)Control).Show = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Control.Opacity = 0d;
|
||||
Control.RenderTransform = new TranslateTransform(0d, -16);
|
||||
aniList.Add(ModAnimation.AaOpacity(Control, 1d, 100, delay,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
aniList.Add(ModAnimation.AaTranslateY(Control, 5d, 250, delay,
|
||||
new ModAnimation.AniEaseOutFluent()));
|
||||
aniList.Add(ModAnimation.AaTranslateY(Control, 11d, 350, delay, new ModAnimation.AniEaseOutBack()));
|
||||
delay += 25;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条动画
|
||||
var scroll = GetFirstScrollViewer(realElements);
|
||||
if (scroll is not null)
|
||||
{
|
||||
if (scroll.RenderTransform is not TranslateTransform)
|
||||
scroll.RenderTransform = new TranslateTransform(10d, 0d);
|
||||
aniList.Add(ModAnimation.AaTranslateX(scroll, -((TranslateTransform)scroll.RenderTransform).X, 350, 0,
|
||||
new ModAnimation.AniEaseOutFluent()));
|
||||
}
|
||||
|
||||
// 结束
|
||||
aniList.Add(ModAnimation.AaCode(() => PageOnEnterAnimationFinished(), after: true));
|
||||
ModAnimation.AniStart(aniList, "PageRight PageChange " + pageUuid, true);
|
||||
}
|
||||
|
||||
// 逐个退出动画
|
||||
public void TriggerExitAnimation(params FrameworkElement[] elements)
|
||||
{
|
||||
var realElements = elements.Where(e => e is not null);
|
||||
var aniList = new List<ModAnimation.AniData>();
|
||||
var delay = 0;
|
||||
foreach (var Element in realElements)
|
||||
foreach (var Control in GetAllAnimControls(Element))
|
||||
if (Control is MyExtraTextButton)
|
||||
{
|
||||
((MyExtraTextButton)Control).Show = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Control.IsHitTestVisible = false;
|
||||
aniList.Add(ModAnimation.AaOpacity(Control, -1, 70, delay));
|
||||
aniList.Add(ModAnimation.AaTranslateY(Control, -6, 70, delay));
|
||||
delay += 15;
|
||||
}
|
||||
|
||||
// 滚动条动画
|
||||
var scroll = GetFirstScrollViewer(realElements);
|
||||
if (scroll is not null)
|
||||
{
|
||||
if (scroll.RenderTransform is not TranslateTransform)
|
||||
scroll.RenderTransform = new TranslateTransform();
|
||||
aniList.Add(ModAnimation.AaTranslateX(scroll, 10d - ((TranslateTransform)scroll.RenderTransform).X, 90, 0,
|
||||
new ModAnimation.AniEaseInFluent()));
|
||||
}
|
||||
|
||||
// 结束
|
||||
aniList.Add(ModAnimation.AaCode(() =>
|
||||
{
|
||||
foreach (var Element in realElements)
|
||||
Element.Visibility = Visibility.Collapsed;
|
||||
PageOnExitAnimationFinished();
|
||||
}, after: true));
|
||||
ModAnimation.AniStart(aniList, "PageRight PageChange " + pageUuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用页面切换动画的控件列表。
|
||||
/// </summary>
|
||||
public List<FrameworkElement> disabledPageAnimControls = new();
|
||||
|
||||
/// <summary>
|
||||
/// 遍历获取所有需要生成动画的控件。
|
||||
/// </summary>
|
||||
internal IEnumerable<FrameworkElement> GetAllAnimControls(FrameworkElement element, bool ignoreInvisibility = false)
|
||||
{
|
||||
var allControls = new List<FrameworkElement>();
|
||||
_GetAllAnimControls(element, ref allControls, ignoreInvisibility);
|
||||
return allControls.Except(disabledPageAnimControls);
|
||||
}
|
||||
|
||||
private void _GetAllAnimControls(FrameworkElement element, ref List<FrameworkElement> allControls,
|
||||
bool ignoreInvisibility)
|
||||
{
|
||||
if (!ignoreInvisibility && element.Visibility == Visibility.Collapsed)
|
||||
return;
|
||||
if (element is MyCard || element is MyHint || element is MyExtraTextButton || element is TextBlock ||
|
||||
element is MyTextButton)
|
||||
{
|
||||
allControls.Add(element);
|
||||
}
|
||||
else if (element is ContentControl)
|
||||
{
|
||||
var content = ((ContentControl)element).Content;
|
||||
if (content is FrameworkElement)
|
||||
_GetAllAnimControls((FrameworkElement)content, ref allControls, ignoreInvisibility);
|
||||
}
|
||||
else if (element is Panel)
|
||||
{
|
||||
foreach (var Element2 in ((Panel)element).Children)
|
||||
if (Element2 is FrameworkElement)
|
||||
_GetAllAnimControls((FrameworkElement)Element2, ref allControls, ignoreInvisibility);
|
||||
}
|
||||
}
|
||||
|
||||
// 查找列表中的第一个滚动条
|
||||
private MyScrollBar GetFirstScrollViewer(IEnumerable<FrameworkElement> elements)
|
||||
{
|
||||
foreach (var Element in elements)
|
||||
{
|
||||
if (Element is MyScrollViewer Viewer)
|
||||
{
|
||||
if (Viewer.ComputedVerticalScrollBarVisibility != Visibility.Visible)
|
||||
continue;
|
||||
return Viewer.scrollBar;
|
||||
}
|
||||
|
||||
foreach (var Control in LogicalTreeHelper.GetChildren(Element))
|
||||
if (Control is MyScrollViewer ChildViewer)
|
||||
{
|
||||
if (ChildViewer.ComputedVerticalScrollBarVisibility != Visibility.Visible)
|
||||
return null;
|
||||
return ChildViewer.scrollBar;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Grid x:Class="PCL.MyRadioBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True">
|
||||
<TextBlock IsHitTestVisible="False" x:Name="LabText" FontSize="13" Padding="0" Margin="26,0,0,0"
|
||||
Foreground="{DynamicResource ColorBrush1}" HorizontalAlignment="Left" VerticalAlignment="Center" />
|
||||
<Ellipse IsHitTestVisible="False" x:Name="ShapeBorder" StrokeThickness="1.1" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" Width="18" Height="18" Stroke="{DynamicResource ColorBrush1}" Margin="1,0,0,0"
|
||||
Fill="{DynamicResource ColorBrushHalfWhite}" />
|
||||
<Ellipse IsHitTestVisible="False" x:Name="ShapeDot" HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Fill="{Binding Stroke, ElementName=ShapeBorder}" Width="0" Height="0" Margin="10,0,0,0"
|
||||
StrokeThickness="0" />
|
||||
</Grid>
|
||||
@@ -0,0 +1,369 @@
|
||||
using System.Collections;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyRadioBox : IMyRadio
|
||||
{
|
||||
public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
public delegate void PreviewCheckEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
// 指向动画
|
||||
|
||||
private const int animationTimeOfMouseIn = 100; // 鼠标指向动画长度
|
||||
private const int animationTimeOfMouseOut = 200; // 鼠标指向动画长度
|
||||
|
||||
private const int animationTimeOfCheck = 150; // 勾选状态变更动画长度
|
||||
|
||||
// 在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变
|
||||
public static readonly DependencyProperty CheckedProperty = DependencyProperty.Register("Checked", typeof(bool),
|
||||
typeof(MyRadioBox), new PropertyMetadata(false, (dRaw, e) =>
|
||||
{
|
||||
var d = (MyRadioBox)dRaw;
|
||||
if (!d.IsLoaded) d.SyncUI();
|
||||
}));
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyRadioBox), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is not null) ((MyRadioBox)sender).LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
private bool allowMouseDown = true;
|
||||
|
||||
// 点击事件
|
||||
|
||||
private bool mouseDowned;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyRadioBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
MouseLeftButtonUp += (_, _) => Radiobox_MouseUp();
|
||||
MouseLeftButtonDown += (_, _) => Radiobox_MouseDown();
|
||||
MouseLeave += (_, _) => Radiobox_MouseLeave();
|
||||
IsEnabledChanged += (_, _) => Radiobox_IsEnabledChanged();
|
||||
MouseEnter += (_, _) => Radiobox_MouseEnterAnimation();
|
||||
MouseLeave += (_, _) => Radiobox_MouseLeaveAnimation();
|
||||
}
|
||||
|
||||
// 自定义属性
|
||||
public bool Checked
|
||||
{
|
||||
get => (bool)GetValue(CheckedProperty);
|
||||
set => SetChecked(value, false);
|
||||
}
|
||||
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
} // 内容
|
||||
|
||||
public event IMyRadio.CheckEventHandler? Check;
|
||||
public event IMyRadio.ChangedEventHandler? Changed;
|
||||
public event PreviewCheckEventHandler? PreviewCheck;
|
||||
public event PreviewChangeEventHandler? PreviewChange;
|
||||
|
||||
/// <summary>
|
||||
/// 手动设置 Checked 属性。
|
||||
/// </summary>
|
||||
/// <param name="value">新的 Checked 属性。</param>
|
||||
/// <param name="user">是否由用户引发。</param>
|
||||
public void SetChecked(bool value, bool user)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Preview 事件
|
||||
if (value && user)
|
||||
{
|
||||
var e = new ModBase.RouteEventArgs(user);
|
||||
PreviewCheck?.Invoke(this, e);
|
||||
if (e.handled)
|
||||
{
|
||||
Radiobox_MouseLeave();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义属性基础
|
||||
var isChanged = false;
|
||||
if (IsLoaded && value != Checked)
|
||||
PreviewChange?.Invoke(this, new ModBase.RouteEventArgs(user));
|
||||
if (value != Checked)
|
||||
{
|
||||
SetValue(CheckedProperty, value);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
// 保证只有一个单选框选中
|
||||
if (Parent is null)
|
||||
return;
|
||||
var radioboxList = new List<MyRadioBox>();
|
||||
var checkedCount = 0;
|
||||
foreach (var Control in ((Panel)Parent).Children) // 收集控件列表与选中个数
|
||||
if (Control is MyRadioBox radioBox)
|
||||
{
|
||||
radioboxList.Add(radioBox);
|
||||
if (radioBox.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;
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
if (isChanged)
|
||||
{
|
||||
if (Checked)
|
||||
Check?.Invoke(this, new ModBase.RouteEventArgs(user));
|
||||
Changed?.Invoke(this, new ModBase.RouteEventArgs(user));
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
// 更改动画
|
||||
SyncUI();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"单选框勾选改变错误",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncUI()
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
if (Checked)
|
||||
{
|
||||
// 由无变有
|
||||
if (ShapeDot.Opacity < 0.01d)
|
||||
ShapeDot.Opacity = 1d;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScale(ShapeBorder, 10d - ShapeBorder.Width, animationTimeOfCheck,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak), absolute: true),
|
||||
ModAnimation.AaScale(ShapeBorder, 8d, animationTimeOfCheck * 2,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.6d), new ModAnimation.AniEaseOutBack(),
|
||||
absolute: true)
|
||||
}, "MyRadioBox Border " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScale(ShapeDot, 9d - ShapeDot.Width,
|
||||
(int)Math.Round(animationTimeOfCheck * 2.6d),
|
||||
ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak), absolute: true),
|
||||
ModAnimation.AaOpacity(ShapeDot, 1d - ShapeDot.Opacity,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.5d), (int)Math.Round(animationTimeOfCheck * 0.6d))
|
||||
}, "MyRadioBox Dot " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty,
|
||||
IsMouseOver ? "ColorBrush3" : IsEnabled ? "ColorBrush2" : "ColorBrushGray4",
|
||||
animationTimeOfCheck),
|
||||
"MyRadioBox BorderColor " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 由有变无
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width, animationTimeOfCheck,
|
||||
ease: new ModAnimation.AniEaseOutFluent(), absolute: true)
|
||||
}, "MyRadioBox Border " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaScale(ShapeDot, -ShapeDot.Width, animationTimeOfCheck,
|
||||
ease: new ModAnimation.AniEaseInFluent(), absolute: true),
|
||||
ModAnimation.AaOpacity(ShapeDot, -ShapeDot.Opacity,
|
||||
(int)Math.Round(animationTimeOfCheck * 0.5d), (int)Math.Round(animationTimeOfCheck * 0.2d))
|
||||
}, "MyRadioBox Dot " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty,
|
||||
IsMouseOver ? "ColorBrush3" : IsEnabled ? "ColorBrush1" : "ColorBrushGray4",
|
||||
animationTimeOfCheck),
|
||||
"MyRadioBox BorderColor " + Uuid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不使用动画
|
||||
ModAnimation.AniStop("MyRadioBox Border " + Uuid);
|
||||
ModAnimation.AniStop("MyRadioBox Dot " + Uuid);
|
||||
ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid);
|
||||
if (Checked)
|
||||
{
|
||||
ShapeDot.Width = 9d;
|
||||
ShapeDot.Height = 9d;
|
||||
ShapeDot.Opacity = 1d;
|
||||
ShapeDot.Margin = new Thickness(5.5d, 0d, 0d, 0d);
|
||||
ShapeBorder.SetResourceReference(Shape.StrokeProperty, IsEnabled ? "ColorBrush2" : "ColorBrushGray4");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShapeDot.Width = 0d;
|
||||
ShapeDot.Height = 0d;
|
||||
ShapeDot.Opacity = 0d;
|
||||
ShapeDot.Margin = new Thickness(10d, 0d, 0d, 0d);
|
||||
ShapeBorder.SetResourceReference(Shape.StrokeProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Radiobox_MouseUp()
|
||||
{
|
||||
if (!mouseDowned)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下单选框:" + Text);
|
||||
SetChecked(true, true);
|
||||
mouseDowned = false;
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushHalfWhite", 100),
|
||||
"MyRadioBox Background " + Uuid);
|
||||
}
|
||||
|
||||
private void Radiobox_MouseDown()
|
||||
{
|
||||
mouseDowned = true;
|
||||
Focus();
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushBg1", 100),
|
||||
"MyRadioBox Background " + Uuid);
|
||||
if (!Checked)
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), absolute: true),
|
||||
"MyRadioBox Border " + Uuid);
|
||||
}
|
||||
|
||||
private void Radiobox_MouseLeave()
|
||||
{
|
||||
if (!mouseDowned)
|
||||
return;
|
||||
mouseDowned = false;
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushHalfWhite", 100),
|
||||
"MyRadioBox Background " + Uuid);
|
||||
if (!Checked)
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), absolute: true),
|
||||
"MyRadioBox Border " + Uuid);
|
||||
}
|
||||
|
||||
private void Radiobox_IsEnabledChanged()
|
||||
{
|
||||
if (ControlVisualHelpers.ShouldAnimate(this)) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
if (IsEnabled)
|
||||
{
|
||||
// 可用
|
||||
Radiobox_MouseLeaveAnimation();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不可用
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, ThemeManager.colorGray4 - ShapeBorder.Stroke,
|
||||
animationTimeOfMouseOut), "MyRadioBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
ThemeManager.colorGray4 - LabText.Foreground, animationTimeOfMouseOut),
|
||||
"MyRadioBox TextColor " + Uuid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStop("MyRadioBox TextColor " + Uuid);
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
|
||||
ShapeBorder.SetResourceReference(Shape.StrokeProperty,
|
||||
IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4");
|
||||
}
|
||||
}
|
||||
|
||||
private void Radiobox_MouseEnterAnimation()
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, "ColorBrush3", animationTimeOfMouseIn),
|
||||
"MyRadioBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", animationTimeOfMouseIn),
|
||||
"MyRadioBox TextColor " + Uuid);
|
||||
}
|
||||
|
||||
private void Radiobox_MouseLeaveAnimation()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return; // MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误
|
||||
if (ControlVisualHelpers.ShouldAnimate(this))
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty,
|
||||
IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4", animationTimeOfMouseOut),
|
||||
"MyRadioBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
IsEnabled ? "ColorBrush1" : "ColorBrushGray4", animationTimeOfMouseOut),
|
||||
"MyRadioBox TextColor " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid);
|
||||
ModAnimation.AniStop("MyRadioBox TextColor " + Uuid);
|
||||
ShapeBorder.SetResourceReference(Shape.StrokeProperty,
|
||||
IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4");
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Border x:Name="PanBack" x:Class="PCL.MyRadioButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Background="{StaticResource ColorBrushSemiTransparent}"
|
||||
CornerRadius="13.5" MinHeight="27" MaxHeight="27">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid x:Name="LogoHost"
|
||||
Width="16" Height="16"
|
||||
Margin="12,0,0,0"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Path x:Name="ShapeLogo"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Stretch="Uniform"
|
||||
Fill="White" />
|
||||
<svgIcon:SvgIcon x:Name="ShapeSvgIcon"
|
||||
Stretch="Uniform"
|
||||
Visibility="Collapsed"
|
||||
IconBrush="{Binding Fill, ElementName=ShapeLogo}" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="LabText"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White"
|
||||
Margin="8,0,12,0"
|
||||
SnapsToDevicePixels="False"
|
||||
UseLayoutRounding="False" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -0,0 +1,509 @@
|
||||
using System.Collections;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.UI.Theme;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
[ContentProperty("Inlines")]
|
||||
public partial class MyRadioButton
|
||||
{
|
||||
public delegate void ChangeEventHandler(MyRadioButton sender, bool raiseByMouse);
|
||||
|
||||
public delegate void CheckEventHandler(MyRadioButton sender, bool raiseByMouse);
|
||||
|
||||
public delegate void PreviewClickEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
public enum ColorState
|
||||
{
|
||||
White,
|
||||
Highlight
|
||||
}
|
||||
|
||||
// 动画
|
||||
|
||||
private const int animationTimeOfMouseIn = 90; // 鼠标指向动画长度
|
||||
private const int animationTimeOfMouseOut = 150; // 鼠标移出动画长度
|
||||
private const int animationTimeOfCheck = 120; // 勾选状态变更动画长度
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyRadioButton), new PropertyMetadata((sender, e) =>
|
||||
{
|
||||
if (sender is MyRadioButton rb && rb.LabText is not null) rb.LabText.Text = (string)e.NewValue;
|
||||
}));
|
||||
|
||||
private bool _Checked; // 是否选中
|
||||
private bool _hasLegacyLogo;
|
||||
private bool isMouseDown;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyRadioButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
RefreshLogoHostVisibility();
|
||||
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
LabText?.Text = (string)GetValue(TextProperty);
|
||||
|
||||
ThemeService.ColorModeChanged += OnColorModeChanged;
|
||||
ThemeService.ColorThemeChanged += OnColorThemeChanged;
|
||||
};
|
||||
|
||||
Unloaded += (_, _) =>
|
||||
{
|
||||
ThemeService.ColorModeChanged -= OnColorModeChanged;
|
||||
ThemeService.ColorThemeChanged -= OnColorThemeChanged;
|
||||
};
|
||||
|
||||
MouseLeftButtonUp += (_, _) => Radiobox_MouseUp();
|
||||
MouseLeftButtonDown += (_, _) => Radiobox_MouseDown();
|
||||
MouseLeave += (_, _) => Radiobox_MouseLeave();
|
||||
MouseEnter += RefreshColor;
|
||||
MouseLeave += RefreshColor;
|
||||
Loaded += RefreshColor;
|
||||
}
|
||||
|
||||
private void OnColorModeChanged(bool isDarkMode, ColorTheme theme)
|
||||
{
|
||||
Dispatcher.Invoke(RefreshMyRadioButtonColor);
|
||||
}
|
||||
|
||||
private void OnColorThemeChanged(ColorTheme theme)
|
||||
{
|
||||
Dispatcher.Invoke(RefreshMyRadioButtonColor);
|
||||
}
|
||||
|
||||
// 自定义属性
|
||||
|
||||
public string Logo
|
||||
{
|
||||
get => ShapeLogo.Data?.ToString() ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (ShapeLogo is null) return;
|
||||
_hasLegacyLogo = !string.IsNullOrWhiteSpace(value);
|
||||
ShapeLogo.Data = _hasLegacyLogo
|
||||
? (Geometry)new GeometryConverter().ConvertFromString(value)
|
||||
: null;
|
||||
SvgIconControlHelper.ApplyVisibility(ShapeLogo, ShapeSvgIcon, IsUsingSvgIcon);
|
||||
RefreshLogoHostVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public string SvgIcon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
value ??= string.Empty;
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
if (ShapeLogo is null || ShapeSvgIcon is null)
|
||||
return;
|
||||
SvgIconControlHelper.ApplyIcon(ShapeLogo, ShapeSvgIcon, field);
|
||||
ApplyLogoScale();
|
||||
RefreshLogoHostVisibility();
|
||||
RefreshColor();
|
||||
}
|
||||
} = string.Empty;
|
||||
|
||||
private bool IsUsingSvgIcon => SvgIconControlHelper.HasSvgIcon(SvgIcon);
|
||||
|
||||
private double EffectiveLogoScale => IsUsingSvgIcon ? 1D : LogoScale;
|
||||
|
||||
private bool HasAnyIcon => IsUsingSvgIcon || _hasLegacyLogo;
|
||||
|
||||
private void ApplyLogoScale()
|
||||
{
|
||||
LogoHost?.RenderTransform = new ScaleTransform
|
||||
{
|
||||
ScaleX = EffectiveLogoScale,
|
||||
ScaleY = EffectiveLogoScale
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshLogoHostVisibility()
|
||||
{
|
||||
if (LogoHost is null || LabText is null)
|
||||
return;
|
||||
|
||||
if (HasAnyIcon)
|
||||
{
|
||||
LogoHost.Visibility = Visibility.Visible;
|
||||
LogoHost.Width = 16;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoHost.Visibility = Visibility.Visible;
|
||||
LogoHost.Width = 0;
|
||||
}
|
||||
|
||||
LogoHost.Height = 16;
|
||||
LogoHost.Margin = new Thickness(12, 0, 0, 0);
|
||||
LabText.Margin = new Thickness(8, 0, 12, 0);
|
||||
}
|
||||
|
||||
public double LogoScale
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ApplyLogoScale();
|
||||
}
|
||||
} = 1d;
|
||||
|
||||
public bool Checked
|
||||
{
|
||||
get => _Checked;
|
||||
set => SetChecked(value, false, true);
|
||||
}
|
||||
|
||||
public InlineCollection Inlines => LabText.Inlines;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
} // 内容
|
||||
|
||||
public ColorState ColorType
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
RefreshColor();
|
||||
}
|
||||
} = ColorState.White; // 颜色类别
|
||||
|
||||
public event CheckEventHandler? Check;
|
||||
|
||||
/// <summary>
|
||||
/// 手动设置 Checked 属性。
|
||||
/// </summary>
|
||||
/// <param name="value">新的 Checked 属性。</param>
|
||||
/// <param name="raiseByMouse">是否由用户引发。</param>
|
||||
/// <param name="anime">是否执行动画。</param>
|
||||
public void SetChecked(bool value, bool raiseByMouse, bool anime)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 自定义属性基础
|
||||
|
||||
var isChanged = false;
|
||||
if (_Checked != value)
|
||||
{
|
||||
_Checked = value;
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
// 保证只有一个单选框选中
|
||||
|
||||
if (Parent is null) return;
|
||||
var radioboxList = new List<MyRadioButton>();
|
||||
var checkedCount = 0;
|
||||
// 收集控件列表与选中个数
|
||||
foreach (var Control in ((Panel)Parent).Children)
|
||||
if (Control is MyRadioButton radioButton)
|
||||
{
|
||||
radioboxList.Add(radioButton);
|
||||
if (radioButton.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;
|
||||
}
|
||||
}
|
||||
|
||||
// 更改动画
|
||||
|
||||
if (!isChanged)
|
||||
return;
|
||||
RefreshColor(null, anime);
|
||||
|
||||
// 触发事件
|
||||
if (Checked)
|
||||
Check?.Invoke(this, raiseByMouse);
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"单选按钮勾选改变错误",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
// 点击事件
|
||||
|
||||
public event PreviewClickEventHandler? PreviewClick;
|
||||
|
||||
private void Radiobox_MouseUp()
|
||||
{
|
||||
if (Checked)
|
||||
return;
|
||||
if (!isMouseDown)
|
||||
return;
|
||||
ModBase.Log("[Control] 按下单选按钮:" + Text);
|
||||
isMouseDown = false;
|
||||
var e = new ModBase.RouteEventArgs(true);
|
||||
PreviewClick?.Invoke(this, e);
|
||||
if (e.handled)
|
||||
return;
|
||||
SetChecked(true, true, true);
|
||||
}
|
||||
|
||||
private void Radiobox_MouseDown()
|
||||
{
|
||||
if (Checked)
|
||||
return;
|
||||
isMouseDown = true;
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
private void Radiobox_MouseLeave()
|
||||
{
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
private void RefreshColor(object obj = null, object e = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0 &&
|
||||
!false.Equals(e)) // 防止默认属性变更触发动画,若强制不执行动画,则 e 为 False
|
||||
{
|
||||
switch (ColorType)
|
||||
{
|
||||
case ColorState.White:
|
||||
{
|
||||
if (Checked)
|
||||
{
|
||||
// 勾选
|
||||
var color3 = new ModBase.MyColor(ThemeManager.AppResources["ColorObject3"]);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, color3 - ShapeLogo.Fill,
|
||||
animationTimeOfCheck),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
color3 - LabText.Foreground, animationTimeOfCheck)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - Background, animationTimeOfCheck),
|
||||
"MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else if (isMouseDown)
|
||||
{
|
||||
// 按下
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty,
|
||||
new ModBase.MyColor(120d,
|
||||
new ModBase.MyColor(ThemeManager.AppResources["ColorObject8"])) - Background, 60),
|
||||
"MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
// 指向
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill, animationTimeOfMouseIn),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground,
|
||||
animationTimeOfMouseIn)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty,
|
||||
new ModBase.MyColor(50d,
|
||||
new ModBase.MyColor(ThemeManager.AppResources["ColorObject8"])) - Background,
|
||||
animationTimeOfMouseIn), "MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 正常
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill,
|
||||
animationTimeOfMouseOut),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground,
|
||||
animationTimeOfMouseOut)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty,
|
||||
new ModBase.MyColor(ThemeManager.AppResources["ColorBrushSemiTransparent"]) -
|
||||
Background, animationTimeOfMouseOut), "MyRadioButton Color " + Uuid);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ColorState.Highlight:
|
||||
{
|
||||
if (Checked)
|
||||
{
|
||||
// 勾选
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill, animationTimeOfCheck),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
|
||||
new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground,
|
||||
animationTimeOfCheck)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush3", animationTimeOfCheck),
|
||||
"MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else if (isMouseDown)
|
||||
{
|
||||
// 按下
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", animationTimeOfMouseIn),
|
||||
"MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
// 指向
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3",
|
||||
animationTimeOfMouseIn),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3",
|
||||
animationTimeOfMouseIn)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush7", animationTimeOfMouseIn),
|
||||
"MyRadioButton Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 正常
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3",
|
||||
animationTimeOfMouseOut),
|
||||
ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3",
|
||||
animationTimeOfMouseOut)
|
||||
}, "MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(this, BackgroundProperty,
|
||||
new ModBase.MyColor(ThemeManager.AppResources["ColorBrushSemiTransparent"]) -
|
||||
Background, animationTimeOfMouseOut), "MyRadioButton Color " + Uuid);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// 不使用动画
|
||||
ModAnimation.AniStop("MyRadioButton Checked " + Uuid);
|
||||
ModAnimation.AniStop("MyRadioButton Color " + Uuid);
|
||||
switch (ColorType)
|
||||
{
|
||||
case ColorState.White:
|
||||
{
|
||||
if (Checked)
|
||||
{
|
||||
Background = new ModBase.MyColor(255d, 255d, 255d);
|
||||
ShapeLogo.SetResourceReference(Shape.FillProperty, "ColorBrush3");
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3");
|
||||
}
|
||||
else
|
||||
{
|
||||
Background = (Brush)ThemeManager.AppResources["ColorBrushSemiTransparent"];
|
||||
ShapeLogo.Fill = new ModBase.MyColor(255d, 255d, 255d);
|
||||
LabText.Foreground = new ModBase.MyColor(255d, 255d, 255d);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ColorState.Highlight:
|
||||
{
|
||||
if (Checked)
|
||||
{
|
||||
SetResourceReference(BackgroundProperty, "ColorBrush3");
|
||||
ShapeLogo.Fill = new ModBase.MyColor(255d, 255d, 255d);
|
||||
LabText.Foreground = new ModBase.MyColor(255d, 255d, 255d);
|
||||
}
|
||||
else
|
||||
{
|
||||
Background = (Brush)ThemeManager.AppResources["ColorBrushSemiTransparent"];
|
||||
ShapeLogo.SetResourceReference(Shape.FillProperty, "ColorBrush3");
|
||||
LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "刷新单选按钮颜色出错");
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshMyRadioButtonColor()
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyScrollBar : ScrollBar
|
||||
{
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyScrollBar()
|
||||
{
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
GotMouseCapture += (_, _) => RefreshColor();
|
||||
LostMouseCapture += (_, _) => RefreshColor();
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
IsVisibleChanged += (_, _) => RefreshColor();
|
||||
}
|
||||
|
||||
// 指向动画
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 判断当前颜色
|
||||
double newOpacity;
|
||||
string newColor;
|
||||
int time;
|
||||
if (!IsVisible)
|
||||
{
|
||||
newOpacity = 0d;
|
||||
time = 20; // 防止错误的尺寸判断导致闪烁
|
||||
newColor = "ColorBrush4";
|
||||
}
|
||||
else if (IsMouseCaptureWithin)
|
||||
{
|
||||
newOpacity = 1d;
|
||||
newColor = "ColorBrush4";
|
||||
time = 50;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
{
|
||||
newOpacity = 0.9d;
|
||||
newColor = "ColorBrush3";
|
||||
time = 130;
|
||||
}
|
||||
else
|
||||
{
|
||||
newOpacity = 0.5d;
|
||||
newColor = "ColorBrush4";
|
||||
time = 180;
|
||||
}
|
||||
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, ForegroundProperty, newColor, time),
|
||||
ModAnimation.AaOpacity(this, newOpacity - Opacity, time)
|
||||
}, "MyScrollBar Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MyScrollBar Color " + Uuid);
|
||||
SetResourceReference(ForegroundProperty, newColor);
|
||||
Opacity = newOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "滚动条颜色改变出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using PCL.Core.UI.Controls;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyScrollViewer : ScrollViewer
|
||||
{
|
||||
private double realOffset;
|
||||
|
||||
public MyScrollBar scrollBar;
|
||||
|
||||
public MyScrollViewer()
|
||||
{
|
||||
PreviewMouseWheel += MyScrollViewer_PreviewMouseWheel;
|
||||
ScrollChanged += MyScrollViewer_ScrollChanged;
|
||||
IsVisibleChanged += MyScrollViewer_IsVisibleChanged;
|
||||
Loaded += (_, _) => Load();
|
||||
PreviewGotKeyboardFocus += MyScrollViewer_PreviewGotKeyboardFocus;
|
||||
}
|
||||
|
||||
public double DeltaMult { get; set; } = 1d;
|
||||
|
||||
private void MyScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (e.Delta == 0 || ScrollableHeight <= 0d)
|
||||
return;
|
||||
|
||||
var src = e.Source;
|
||||
if (Content is FrameworkElement element && element.TemplatedParent is null)
|
||||
{
|
||||
switch (src)
|
||||
{
|
||||
case ComboBox { IsDropDownOpen: true }:
|
||||
case TextBox { AcceptsReturn: true }:
|
||||
case ComboBoxItem:
|
||||
case CheckBox:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
PerformVerticalOffsetDelta(-e.Delta);
|
||||
|
||||
Tooltip.Dismiss();
|
||||
}
|
||||
|
||||
public void PerformVerticalOffsetDelta(double delta)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaDouble(animDelta =>
|
||||
{
|
||||
realOffset = ModBase.MathClamp(realOffset + (double)animDelta, 0d, ExtentHeight - ActualHeight);
|
||||
ScrollToVerticalOffset(realOffset);
|
||||
}, delta * DeltaMult, 300, 0, new ModAnimation.AniEaseOutFluent((ModAnimation.AniEasePower)6), false));
|
||||
}
|
||||
|
||||
private void MyScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
realOffset = VerticalOffset;
|
||||
if (ModMain.frmMain is not null &&
|
||||
(e.VerticalChange != 0 || e.ViewportHeightChange != 0))
|
||||
ModMain.frmMain.BtnExtraBack.ShowRefresh();
|
||||
}
|
||||
|
||||
private void MyScrollViewer_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
ModMain.frmMain.BtnExtraBack.ShowRefresh();
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
scrollBar = (MyScrollBar)GetTemplateChild("PART_VerticalScrollBar");
|
||||
}
|
||||
|
||||
private void MyScrollViewer_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
if (e.NewFocus is MySlider)
|
||||
e.Handled = true; // #3854,阻止获得焦点时自动滚动
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<local:MyCard x:Class="PCL.MySearchBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
Height="40">
|
||||
<Grid>
|
||||
<local:MyTextBox HasBackground="False" x:Name="TextBox" BorderThickness="0" Padding="32,0,40,0" MaxLength="50"
|
||||
FontSize="13.5" UseLayoutRounding="False" SnapsToDevicePixels="False"
|
||||
VerticalContentAlignment="Center"
|
||||
TextChanged="Text_TextChanged" />
|
||||
<svgIcon:SvgIcon Width="14" Margin="14,0,0,0" IsHitTestVisible="False" Height="14"
|
||||
IconBrush="{DynamicResource ColorBrush1}" Icon="lucide/search"
|
||||
HorizontalAlignment="Left" />
|
||||
<local:MyIconButton x:Name="BtnClear" Click="BtnClear_Click" IsHitTestVisible="False" Opacity="0" Width="24" Margin="0,0,10,0"
|
||||
LogoScale="0.75" Height="24"
|
||||
SvgIcon="lucide/x"
|
||||
HorizontalAlignment="Right" Theme="Black" />
|
||||
<local:MyButton x:Name="BtnSearch" Click="BtnSearch_Click" Visibility="Collapsed" Margin="0,4,4,4" Text="{DynamicResource Common.Action.Search}" Width="60"
|
||||
ColorType="Highlight" HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</local:MyCard>
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MySearchBox : MyCard
|
||||
{
|
||||
public delegate void SearchEventHandler(object sender, EventArgs e);
|
||||
|
||||
public delegate void TextChangedEventHandler(object sender, EventArgs e);
|
||||
|
||||
public MySearchBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Loaded += MySearchBox_Loaded;
|
||||
}
|
||||
|
||||
private void MySearchBox_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter) ModMain.RaiseCustomEvent(this);
|
||||
}
|
||||
|
||||
// 属性
|
||||
public string HintText
|
||||
{
|
||||
get => (string)GetValue(HintTextProperty);
|
||||
set => SetValue(HintTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HintTextProperty =
|
||||
DependencyProperty.Register("HintText", typeof(string), typeof(MySearchBox),
|
||||
new PropertyMetadata(string.Empty, (d, e) => ((MySearchBox)d).TextBox.HintText = (string)e.NewValue));
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register("Text", typeof(string), typeof(MySearchBox),
|
||||
new PropertyMetadata(string.Empty, (d, e) => ((MySearchBox)d).TextBox.Text = (string)e.NewValue));
|
||||
|
||||
public Visibility SearchButtonVisibility
|
||||
{
|
||||
get => BtnSearch.Visibility;
|
||||
set
|
||||
{
|
||||
BtnClear.Margin = new Thickness(0d, 0d, value == Visibility.Visible ? 70 : 10, 0d);
|
||||
BtnSearch.Visibility = value;
|
||||
}
|
||||
}
|
||||
|
||||
public event TextChangedEventHandler? TextChanged;
|
||||
|
||||
private void MySearchBox_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TextBox.Focus();
|
||||
}
|
||||
|
||||
private void Text_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
UpdateClearButtonState();
|
||||
SetCurrentValue(TextProperty, TextBox.Text);
|
||||
|
||||
TextChanged?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void BtnClear_Click(object sender, EventArgs e)
|
||||
{
|
||||
TextBox.Text = "";
|
||||
TextBox.Focus();
|
||||
}
|
||||
|
||||
public event SearchEventHandler? Search;
|
||||
|
||||
private void BtnSearch_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Search?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void UpdateClearButtonState()
|
||||
{
|
||||
var hasText = !string.IsNullOrEmpty(TextBox.Text);
|
||||
ModAnimation.AniStart(ModAnimation.AaOpacity(BtnClear, hasText ? 1d - BtnClear.Opacity : -BtnClear.Opacity, 90),
|
||||
"MySearchBox ClearBtn " + uuid);
|
||||
BtnClear.IsHitTestVisible = hasText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Border x:Class="PCL.MySlider"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="16" d:DesignWidth="162"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
MinHeight="16" UseLayoutRounding="False" SnapsToDevicePixels="False"
|
||||
BorderBrush="{DynamicResource ColorBrushBg0}" Name="PanBack"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True">
|
||||
<Grid x:Name="PanMain" MinHeight="16" MaxHeight="16" IsHitTestVisible="False">
|
||||
<Line Height="4" Name="LineBack" Stroke="{DynamicResource ColorBrushBg0}" Opacity="0.3" StrokeThickness="1"
|
||||
X2="10000" Y1="2" Y2="2" HorizontalAlignment="Right" Width="0" SnapsToDevicePixels="True"
|
||||
Margin="0,0,1,0" />
|
||||
<Line Height="4" Name="LineFore" Stroke="{Binding ElementName=PanBack, Path=BorderBrush}" StrokeThickness="2"
|
||||
X2="10000" Y1="2" Y2="2" Width="0" HorizontalAlignment="Left" SnapsToDevicePixels="True" Margin="1,0,0,0" />
|
||||
<Ellipse HorizontalAlignment="Left" Width="10" Height="10"
|
||||
Stroke="{Binding BorderBrush, ElementName=PanBack}" StrokeThickness="1.2"
|
||||
Fill="{DynamicResource ColorBrushBg0}" Name="ShapeDot" VerticalAlignment="Center"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Ellipse.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
<Popup Name="Popup" AllowsTransparency="True" IsHitTestVisible="False" PopupAnimation="Fade"
|
||||
VerticalOffset="38" HorizontalOffset="{Binding Margin.Left, ElementName=ShapeDot}" Placement="Center"
|
||||
PlacementRectangle="8,0,0,0">
|
||||
<Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1.5" CornerRadius="3" MaxWidth="500"
|
||||
Background="{DynamicResource ColorBrushWhite}" Margin="0,0,4,4" SnapsToDevicePixels="True">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Opacity="0.4" BlurRadius="4" ShadowDepth="2" />
|
||||
</Border.Effect>
|
||||
<TextBlock Name="TextHint" TextWrapping="Wrap" Foreground="{DynamicResource ColorBrush1}" Margin="7,5"
|
||||
FontSize="12" />
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -0,0 +1,298 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public partial class MySlider
|
||||
{
|
||||
public delegate void ChangeEventHandler(object sender, bool user);
|
||||
|
||||
public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e);
|
||||
|
||||
// 自定义属性
|
||||
|
||||
private int _Value;
|
||||
private bool changeByKey;
|
||||
|
||||
// 拖动
|
||||
|
||||
public Delegate getHintText;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MySlider()
|
||||
{
|
||||
InitializeComponent();
|
||||
SizeChanged += RefreshWidth;
|
||||
MouseLeftButtonDown += DragStart;
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
MouseEnter += (_, _) => MySlider_MouseEnter();
|
||||
KeyDown += MySlider_KeyDown;
|
||||
}
|
||||
|
||||
public int MaxValue
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value == field)
|
||||
return;
|
||||
field = value;
|
||||
RefreshWidth(null, null);
|
||||
}
|
||||
} = 100;
|
||||
|
||||
public int Value
|
||||
{
|
||||
get => _Value;
|
||||
set
|
||||
{
|
||||
try
|
||||
{
|
||||
value = (int)Math.Round(ModBase.MathClamp(value, 0d, MaxValue));
|
||||
if (_Value == value)
|
||||
return;
|
||||
|
||||
// 触发 Preview 事件,修改新值
|
||||
var oldValue = _Value;
|
||||
_Value = value;
|
||||
if (ModAnimation.AniControlEnabled == 0)
|
||||
{
|
||||
var e = new ModBase.RouteEventArgs();
|
||||
PreviewChange?.Invoke(this, e);
|
||||
if (e.handled)
|
||||
{
|
||||
_Value = oldValue;
|
||||
DragStop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0)
|
||||
{
|
||||
if (ActualWidth < ShapeDot.Width)
|
||||
return;
|
||||
var newWidth = _Value / (double)MaxValue * (ActualWidth - ShapeDot.Width);
|
||||
var deltaProcess =
|
||||
Math.Abs(LineFore.Width / (ActualWidth - ShapeDot.Width) - _Value / (double)MaxValue);
|
||||
var time = (1d - Math.Pow(1d - deltaProcess, 3d)) * 300d + (changeByKey ? 100 : 0);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaWidth(LineFore,
|
||||
Math.Max(0d, newWidth + (newWidth < 0.5d ? 0d : 0.5d)) - LineFore.Width,
|
||||
(int)Math.Round(time),
|
||||
ease: time > 50d
|
||||
? new ModAnimation.AniEaseOutFluent()
|
||||
: new ModAnimation.AniEaseLinear()),
|
||||
ModAnimation.AaWidth(LineBack,
|
||||
Math.Max(0d,
|
||||
ActualWidth - ShapeDot.Width - newWidth +
|
||||
(ActualWidth - ShapeDot.Width - newWidth < 0.5d ? 0d : 0.5d)) - LineBack.Width,
|
||||
(int)Math.Round(time),
|
||||
ease: time > 50d
|
||||
? new ModAnimation.AniEaseOutFluent()
|
||||
: new ModAnimation.AniEaseLinear()),
|
||||
ModAnimation.AaX(ShapeDot, newWidth - ShapeDot.Margin.Left, (int)Math.Round(time),
|
||||
ease: time > 50d
|
||||
? new ModAnimation.AniEaseOutFluent()
|
||||
: new ModAnimation.AniEaseLinear())
|
||||
}, "MySlider Progress " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshWidth(null, null);
|
||||
}
|
||||
|
||||
if (ModAnimation.AniControlEnabled == 0)
|
||||
Change?.Invoke(this, false);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"滑动条进度改变出错",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Control.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按键改变
|
||||
|
||||
public uint ValueByKey { get; set; } = 1U;
|
||||
public event ChangeEventHandler? Change;
|
||||
public event PreviewChangeEventHandler? PreviewChange;
|
||||
|
||||
private void RefreshWidth(object sender, SizeChangedEventArgs? e)
|
||||
{
|
||||
if (e is not null)
|
||||
PanMain.Width = e.NewSize.Width;
|
||||
ModAnimation.AniStop("MySlider Progress " + Uuid);
|
||||
var newWidth = _Value / (double)MaxValue * (ActualWidth - ShapeDot.Width);
|
||||
LineFore.Width = Math.Max(0d, newWidth + (newWidth < 0.5d ? 0d : 0.5d));
|
||||
LineBack.Width = Math.Max(0d,
|
||||
ActualWidth - ShapeDot.Width - newWidth + (ActualWidth - ShapeDot.Width - newWidth < 0.5d ? 0d : 0.5d));
|
||||
ModBase.SetLeft(ShapeDot, newWidth);
|
||||
}
|
||||
|
||||
private void DragStart(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CaptureMouse();
|
||||
MouseMove += OnDragMouseMove;
|
||||
e.Handled = true; // 防止 ScrollViewer 失焦问题
|
||||
ModMain.dragControl = this;
|
||||
RefreshColor();
|
||||
ModMain.frmMain.DragDoing();
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeDot, 1.3d - ((ScaleTransform)ShapeDot.RenderTransform).ScaleX, 40,
|
||||
ease: new ModAnimation.AniEaseOutFluent()), "MySlider Scale " + Uuid);
|
||||
RefreshPopup();
|
||||
ModAnimation.AniStop("MySlider KeyPopup " + Uuid);
|
||||
}
|
||||
|
||||
public void DragDoing()
|
||||
{
|
||||
var percent =
|
||||
ModBase.MathClamp((Mouse.GetPosition(PanMain).X - ShapeDot.Width / 2d) / (ActualWidth - ShapeDot.Width), 0d,
|
||||
1d);
|
||||
var newValue = (int)Math.Round(percent * MaxValue);
|
||||
if (newValue != Value) Value = newValue;
|
||||
RefreshPopup();
|
||||
}
|
||||
|
||||
private void OnDragMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
DragDoing();
|
||||
}
|
||||
|
||||
public void DragStop()
|
||||
{
|
||||
MouseMove -= OnDragMouseMove;
|
||||
if (IsMouseCaptured) ReleaseMouseCapture();
|
||||
RefreshColor();
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(ShapeDot, 1d - ((ScaleTransform)ShapeDot.RenderTransform).ScaleX, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent()), "MySlider Scale " + Uuid);
|
||||
Popup.IsOpen = false;
|
||||
}
|
||||
|
||||
public void RefreshPopup()
|
||||
{
|
||||
if (getHintText is null)
|
||||
return;
|
||||
Popup.IsOpen = true;
|
||||
TextHint.Text = getHintText.DynamicInvoke(Value)?.ToString() ?? "";
|
||||
var typeface = new Typeface(TextHint.FontFamily, TextHint.FontStyle, TextHint.FontWeight, TextHint.FontStretch);
|
||||
var formattedText = new FormattedText(TextHint.Text, Thread.CurrentThread.CurrentCulture,
|
||||
TextHint.FlowDirection, typeface, TextHint.FontSize, TextHint.Foreground, ModBase.dpi);
|
||||
TextHint.Width = formattedText.Width; // 使用手动测量的宽度修复 #1057
|
||||
}
|
||||
|
||||
// 指向动画
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 判断当前颜色
|
||||
string foregroundName;
|
||||
string dotFillName;
|
||||
int animationTime;
|
||||
if (IsEnabled)
|
||||
{
|
||||
if (ModMain.dragControl is not null && ModMain.dragControl.Equals(this) || IsMouseOver)
|
||||
{
|
||||
foregroundName = "ColorBrush3";
|
||||
dotFillName = "ColorBrush3";
|
||||
animationTime = 40;
|
||||
}
|
||||
else
|
||||
{
|
||||
foregroundName = "ColorBrushBg0";
|
||||
dotFillName = "ColorBrushBg0";
|
||||
animationTime = 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foregroundName = "ColorBrushGray5";
|
||||
dotFillName = "ColorBrushGray5";
|
||||
animationTime = 200;
|
||||
}
|
||||
|
||||
// 触发颜色动画
|
||||
if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
|
||||
{
|
||||
// 有动画
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaColor(this, BorderBrushProperty, foregroundName, animationTime),
|
||||
ModAnimation.AaColor(ShapeDot, Shape.FillProperty, dotFillName, animationTime)
|
||||
}, "MySlider Color " + Uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniStop("MySlider Color " + Uuid);
|
||||
SetResourceReference(BorderBrushProperty, foregroundName);
|
||||
ShapeDot.SetResourceReference(Shape.FillProperty, dotFillName);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "滑动条颜色改变出错");
|
||||
}
|
||||
}
|
||||
|
||||
private void MySlider_MouseEnter()
|
||||
{
|
||||
Focus(); // 确保按键能改变值
|
||||
}
|
||||
|
||||
private void MySlider_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
// 拒绝一边拖动一边用按键改变
|
||||
if (ReferenceEquals(this, ModMain.dragControl))
|
||||
return;
|
||||
// 改变值
|
||||
if (e.Key == Key.Left)
|
||||
{
|
||||
changeByKey = true;
|
||||
Value = (int)(Value - ValueByKey);
|
||||
changeByKey = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Right)
|
||||
{
|
||||
changeByKey = true;
|
||||
Value = (int)(Value + ValueByKey);
|
||||
changeByKey = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 Popup
|
||||
if (getHintText is not null)
|
||||
{
|
||||
RefreshPopup();
|
||||
ModAnimation.AniStop("MySlider KeyPopup " + Uuid);
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaCode(() => Popup.IsOpen = false, (int)Math.Round(700d * ModAnimation.aniSpeed)),
|
||||
"MySlider KeyPopup " + Uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyTextButton : Label
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, EventArgs e);
|
||||
|
||||
// 指向动画
|
||||
|
||||
private const int animationTimeIn = 100;
|
||||
private const int animationTimeOut = 200;
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
|
||||
typeof(MyTextButton), new PropertyMetadata("", (sender, e) =>
|
||||
{
|
||||
if (Equals(e.OldValue, e.NewValue)) return;
|
||||
var button = (MyTextButton)sender;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(button, -button.Opacity, 50),
|
||||
ModAnimation.AaCode(() => button.Content = e.NewValue, after: true),
|
||||
ModAnimation.AaOpacity(button, 1d, 170)
|
||||
}, "MyTextButton Text " + button.Uuid);
|
||||
}));
|
||||
|
||||
private string colorName;
|
||||
|
||||
// 鼠标事件
|
||||
|
||||
public bool isMouseDown;
|
||||
|
||||
// 基础
|
||||
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public MyTextButton()
|
||||
{
|
||||
SetResourceReference(ForegroundProperty, "ColorBrush1");
|
||||
Background = ThemeManager.colorSemiTransparent;
|
||||
PreviewMouseLeftButtonDown += MyTextButton_MouseLeftButtonDown;
|
||||
MouseLeave += (_, _) => MyTextButton_MouseLeave();
|
||||
PreviewMouseLeftButtonUp += MyTextButton_MouseLeftButtonUp;
|
||||
MouseEnter += (_, _) => RefreshColor();
|
||||
MouseLeave += (_, _) => RefreshColor();
|
||||
IsEnabledChanged += (_, _) => RefreshColor();
|
||||
MouseLeftButtonDown += (_, _) => RefreshColor();
|
||||
MouseLeftButtonUp += (_, _) => RefreshColor();
|
||||
}
|
||||
|
||||
// 文本
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
private (string ForeName, int Time) GetVisualState()
|
||||
{
|
||||
if (isMouseDown)
|
||||
return ("ColorBrush4", 30);
|
||||
if (IsMouseOver)
|
||||
return ("ColorBrush3", animationTimeIn);
|
||||
return ("ColorBrush1", animationTimeOut);
|
||||
}
|
||||
|
||||
private void MyTextButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isMouseDown = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void MyTextButton_MouseLeave()
|
||||
{
|
||||
isMouseDown = false;
|
||||
}
|
||||
|
||||
private void MyTextButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isMouseDown) return;
|
||||
isMouseDown = false;
|
||||
ModBase.Log("[Control] 按下文本按钮:" + Text);
|
||||
Click?.Invoke(this, null);
|
||||
ModMain.RaiseCustomEvent(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void RefreshColor()
|
||||
{
|
||||
var (ForeName, Time) = GetVisualState();
|
||||
|
||||
// 重复性验证
|
||||
if ((colorName ?? "") == (ForeName ?? ""))
|
||||
return;
|
||||
colorName = ForeName;
|
||||
// 触发颜色动画
|
||||
ControlVisualHelpers.AnimateColorOrSetResource(this, ForegroundProperty, ForeName, Time,
|
||||
"MyTextButton Color " + Uuid, ControlVisualHelpers.ShouldAnimate(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Border x:Class="PCL.MyToast"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:svgIcon="clr-namespace:PCL.Core.UI.Controls.SvgIcon;assembly=PCL.Core"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
x:Name="Root"
|
||||
HorizontalAlignment="Right"
|
||||
CornerRadius="8" BorderThickness="0"
|
||||
UseLayoutRounding="True" SnapsToDevicePixels="True">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="16" Opacity="0.2" ShadowDepth="4" Direction="270" Color="#000000" />
|
||||
</Border.Effect>
|
||||
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="3" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="10,8,10,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<svgIcon:SvgIcon x:Name="ToastIcon" Width="20" Height="20" VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" UseOriginalColor="False" />
|
||||
|
||||
<TextBlock x:Name="TitleText" Grid.Column="1" VerticalAlignment="Center"
|
||||
FontSize="13" FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<local:MyIconButton Grid.Column="2" x:Name="BtnClose" Width="20" Height="20"
|
||||
Margin="8,0,0,0" SvgIcon="lucide/x" Theme="Custom" />
|
||||
</Grid>
|
||||
|
||||
<Rectangle x:Name="ProgressBar" Height="3" Grid.Row="1"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Bottom"
|
||||
RenderTransformOrigin="0,0.5">
|
||||
<Rectangle.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</Rectangle.RenderTransform>
|
||||
</Rectangle>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -0,0 +1,431 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.UI.Theme;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyToast
|
||||
{
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
/// <summary>判定为拖动而非点击的最小水平位移(像素)。</summary>
|
||||
private const double DragDeadzone = 4d;
|
||||
|
||||
/// <summary>拖动时透明度下限,确保控件始终可见。</summary>
|
||||
private const double DragOpacityFloor = 0.35d;
|
||||
|
||||
/// <summary>触发关闭的位移占控件宽度的比例。</summary>
|
||||
private const double DismissThresholdRatio = 0.12d;
|
||||
|
||||
/// <summary>触发关闭的最小绝对位移(像素)。</summary>
|
||||
private const double DismissThresholdMin = 24d;
|
||||
|
||||
/// <summary>拖动释放后,若剩余显示时间不足此值则直接关闭。</summary>
|
||||
private const double MinRemainingMs = 300d;
|
||||
|
||||
/// <summary>拖动释放后回到原位的动画时长(毫秒)。</summary>
|
||||
private const int ReturnAnimationMs = 150;
|
||||
|
||||
// 拖动状态
|
||||
private bool _dragPending;
|
||||
private bool _isDragging;
|
||||
private Point _dragStartPoint;
|
||||
private double _dragStartTranslateX;
|
||||
private FrameworkElement? _dragReference;
|
||||
|
||||
// 进度条状态
|
||||
private double _progressStartWidth;
|
||||
private double _progressTotalMs;
|
||||
|
||||
public MyToast()
|
||||
{
|
||||
InitializeComponent();
|
||||
BtnClose.Click += (_, _) => Dismiss();
|
||||
PreviewMouseLeftButtonDown += Toast_PreviewMouseLeftButtonDown;
|
||||
PreviewMouseMove += Toast_PreviewMouseMove;
|
||||
PreviewMouseLeftButtonUp += Toast_PreviewMouseLeftButtonUp;
|
||||
LostMouseCapture += Toast_LostMouseCapture;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
UpdateColors();
|
||||
ThemeService.ColorModeChanged += OnThemeChanged;
|
||||
ThemeService.ColorThemeChanged += OnColorThemeChanged;
|
||||
};
|
||||
Unloaded += (_, _) =>
|
||||
{
|
||||
ThemeService.ColorModeChanged -= OnThemeChanged;
|
||||
ThemeService.ColorThemeChanged -= OnColorThemeChanged;
|
||||
ModAnimation.AniStop($"Toast Show {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Hide {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Dismiss {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
|
||||
ProgressBar.BeginAnimation(WidthProperty, null);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnThemeChanged(bool isDarkMode, ColorTheme theme) => UpdateColors();
|
||||
private void OnColorThemeChanged(ColorTheme theme) => UpdateColors();
|
||||
|
||||
public string Context
|
||||
{
|
||||
get => TitleText.Text;
|
||||
set => TitleText.Text = value;
|
||||
}
|
||||
|
||||
public string Icon { get; set; } = "lucide/info";
|
||||
|
||||
public HintType ToastType { get; set; } = HintType.Info;
|
||||
|
||||
public double DisplayDuration { get; set; } = 5000;
|
||||
|
||||
public bool IsDismissing { get; private set; }
|
||||
|
||||
private double _targetHeight;
|
||||
|
||||
public void Show()
|
||||
{
|
||||
if (Parent is not Panel)
|
||||
return;
|
||||
if (System.Windows.Application.Current.MainWindow is not null)
|
||||
MaxWidth = System.Windows.Application.Current.MainWindow.ActualWidth * 0.9;
|
||||
Margin = new Thickness(0, 0, 16, 4);
|
||||
Opacity = 0;
|
||||
|
||||
Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
Arrange(new Rect(0, 0, DesiredSize.Width, DesiredSize.Height));
|
||||
_targetHeight = Math.Max(ActualHeight, 45d);
|
||||
Height = 0;
|
||||
|
||||
RenderTransform = new TranslateTransform(60, 0);
|
||||
|
||||
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
|
||||
var enterAnimations = new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaTranslateX(this, -60, 400, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaHeight(this, _targetHeight, 150, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaOpacity(this, 1, 100)
|
||||
};
|
||||
ModAnimation.AniStart(enterAnimations, $"Toast Show {Uuid}");
|
||||
|
||||
RestartHideAnimation();
|
||||
}
|
||||
|
||||
public void Emphasize()
|
||||
{
|
||||
ModAnimation.AniStop($"Toast Show {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Hide {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
|
||||
ProgressBar.BeginAnimation(WidthProperty, null);
|
||||
if (RenderTransform is TranslateTransform tt) tt.X = 0;
|
||||
Opacity = 1;
|
||||
Height = _targetHeight;
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaTranslateX(this, -8, 70, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaTranslateX(this, 16, 70, after: true),
|
||||
ModAnimation.AaTranslateX(this, -8, 60, after: true, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(RestartHideAnimation, after: true),
|
||||
}, $"Toast Emphasize {Uuid}");
|
||||
}
|
||||
|
||||
private void RestartHideAnimation()
|
||||
{
|
||||
StartHideAnimation(DisplayDuration);
|
||||
StartProgressAnimation(DisplayDuration);
|
||||
}
|
||||
|
||||
private void StartHideAnimation(double delayMs)
|
||||
{
|
||||
var delay = (int)Math.Round(delayMs);
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaTranslateX(this, 60, 200, delay, new ModAnimation.AniEaseInFluent()),
|
||||
ModAnimation.AaOpacity(this, -1, 150, delay),
|
||||
ModAnimation.AaHeight(this, -_targetHeight, 100, ease: new ModAnimation.AniEaseOutFluent(), after: true),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (Parent is Panel p)
|
||||
p.Children.Remove(this);
|
||||
}, after: true)
|
||||
}, $"Toast Hide {Uuid}");
|
||||
}
|
||||
|
||||
public void Dismiss()
|
||||
{
|
||||
if (IsDismissing) return;
|
||||
IsDismissing = true;
|
||||
_isDragging = false;
|
||||
_dragPending = false;
|
||||
if (IsMouseCaptured) ReleaseMouseCapture();
|
||||
ModAnimation.AniStop($"Toast Show {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Hide {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
|
||||
ProgressBar.BeginAnimation(WidthProperty, null);
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaTranslateX(this, 60, 150, ease: new ModAnimation.AniEaseInFluent()),
|
||||
ModAnimation.AaOpacity(this, -1, 100),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (Parent is Panel p)
|
||||
p.Children.Remove(this);
|
||||
}, after: true)
|
||||
}, $"Toast Dismiss {Uuid}");
|
||||
}
|
||||
|
||||
private void StartProgressAnimation(double duration)
|
||||
{
|
||||
var totalMs = (int)Math.Round(duration);
|
||||
if (totalMs <= 0)
|
||||
return;
|
||||
var w = ProgressBar.ActualWidth;
|
||||
if (w <= 0) w = 300;
|
||||
ProgressBar.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
ProgressBar.Width = w;
|
||||
_progressStartWidth = w;
|
||||
_progressTotalMs = totalMs;
|
||||
var anim = new DoubleAnimation(w, 0d, TimeSpan.FromMilliseconds(totalMs));
|
||||
ProgressBar.BeginAnimation(WidthProperty, anim);
|
||||
}
|
||||
|
||||
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
RootGrid.Clip = new RectangleGeometry(new Rect(0, 0, RootGrid.ActualWidth, RootGrid.ActualHeight), 8, 8);
|
||||
}
|
||||
|
||||
private void UpdateColors()
|
||||
{
|
||||
var baseHue = ToastType switch
|
||||
{
|
||||
HintType.Success => 145d,
|
||||
HintType.Error => 355d,
|
||||
HintType.Warning => 40d,
|
||||
_ => 210d
|
||||
};
|
||||
var res = System.Windows.Application.Current.Resources;
|
||||
var accent = new ModBase.MyColor().FromHSL2(baseHue, 75, 60);
|
||||
var bg = ThemeService.IsDarkMode
|
||||
? new SolidColorBrush(LabColor.FromLch(0.35))
|
||||
: (Brush)res["ColorBrushBackground"];
|
||||
var text = (SolidColorBrush)res["ColorBrushGray1"];
|
||||
var accentBrush = new SolidColorBrush(accent);
|
||||
|
||||
Root.Background = bg;
|
||||
Root.BorderBrush = bg;
|
||||
TitleText.Foreground = text;
|
||||
ProgressBar.Fill = accentBrush;
|
||||
BtnClose.Foreground = text;
|
||||
ToastIcon.Icon = Icon;
|
||||
ToastIcon.IconBrush = accentBrush;
|
||||
ToastIcon.StrokeThickness = 0;
|
||||
}
|
||||
|
||||
#region 拖动关闭
|
||||
|
||||
private void Toast_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_dragPending = false;
|
||||
if (IsDismissing)
|
||||
return;
|
||||
if (IsDescendantOf(e.OriginalSource as DependencyObject, BtnClose))
|
||||
return;
|
||||
_dragReference = Parent as FrameworkElement;
|
||||
if (_dragReference is null)
|
||||
return;
|
||||
_dragPending = true;
|
||||
_isDragging = false;
|
||||
_dragStartPoint = e.GetPosition(_dragReference);
|
||||
}
|
||||
|
||||
private void Toast_PreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
if (Mouse.LeftButton != MouseButtonState.Pressed || _dragReference is null)
|
||||
{
|
||||
_isDragging = false;
|
||||
_dragPending = false;
|
||||
if (IsMouseCaptured) ReleaseMouseCapture();
|
||||
ReturnFromDrag();
|
||||
return;
|
||||
}
|
||||
var dragCurrent = e.GetPosition(_dragReference);
|
||||
UpdateDragPosition(dragCurrent.X - _dragStartPoint.X);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_dragPending)
|
||||
return;
|
||||
if (Mouse.LeftButton != MouseButtonState.Pressed || _dragReference is null)
|
||||
{
|
||||
_dragPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var current = e.GetPosition(_dragReference);
|
||||
var delta = current.X - _dragStartPoint.X;
|
||||
|
||||
if (delta < DragDeadzone)
|
||||
return;
|
||||
|
||||
BeginDrag(delta);
|
||||
}
|
||||
|
||||
private void Toast_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_dragPending && !_isDragging)
|
||||
{
|
||||
_dragPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isDragging)
|
||||
return;
|
||||
|
||||
_isDragging = false;
|
||||
_dragPending = false;
|
||||
e.Handled = true;
|
||||
|
||||
if (IsMouseCaptured)
|
||||
ReleaseMouseCapture();
|
||||
|
||||
var currentX = (RenderTransform as TranslateTransform)?.X ?? 0d;
|
||||
if (currentX - _dragStartTranslateX >= GetDismissThreshold())
|
||||
{
|
||||
Dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnFromDrag();
|
||||
}
|
||||
|
||||
private void Toast_LostMouseCapture(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isDragging)
|
||||
return;
|
||||
_isDragging = false;
|
||||
_dragPending = false;
|
||||
ReturnFromDrag();
|
||||
}
|
||||
|
||||
private void BeginDrag(double initialDelta)
|
||||
{
|
||||
_isDragging = true;
|
||||
_dragPending = false;
|
||||
|
||||
ModAnimation.AniStop($"Toast Show {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Hide {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
|
||||
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
|
||||
|
||||
PauseProgress();
|
||||
|
||||
Height = _targetHeight;
|
||||
_dragStartTranslateX = (RenderTransform as TranslateTransform)?.X ?? 0d;
|
||||
|
||||
CaptureMouse();
|
||||
|
||||
UpdateDragPosition(initialDelta);
|
||||
}
|
||||
|
||||
private void UpdateDragPosition(double delta)
|
||||
{
|
||||
var newX = _dragStartTranslateX + ApplyDragResistance(delta);
|
||||
if (RenderTransform is TranslateTransform tt)
|
||||
tt.X = newX;
|
||||
Opacity = GetDragOpacity(newX);
|
||||
}
|
||||
|
||||
private void ReturnFromDrag()
|
||||
{
|
||||
if (Parent is null || IsDismissing)
|
||||
return;
|
||||
var currentX = (RenderTransform as TranslateTransform)?.X ?? 0d;
|
||||
var currentOpacity = Opacity;
|
||||
|
||||
var remaining = GetProgressRemainingMs();
|
||||
if (remaining < MinRemainingMs)
|
||||
{
|
||||
Dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
ResumeProgress(remaining);
|
||||
|
||||
ModAnimation.AniStart(new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaTranslateX(this, -currentX, ReturnAnimationMs, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaOpacity(this, 1d - currentOpacity, ReturnAnimationMs),
|
||||
ModAnimation.AaCode(() => StartHideAnimation(GetProgressRemainingMs()), after: true)
|
||||
}, $"Toast Drag Return {Uuid}");
|
||||
}
|
||||
|
||||
private static double ApplyDragResistance(double delta)
|
||||
{
|
||||
return Math.Max(0d, delta);
|
||||
}
|
||||
|
||||
private double GetDragOpacity(double translateX)
|
||||
{
|
||||
if (translateX <= 0d)
|
||||
return 1d;
|
||||
var width = ActualWidth > 0 ? ActualWidth : 1d;
|
||||
return Math.Max(DragOpacityFloor, 1d - (translateX / width) * (1d - DragOpacityFloor));
|
||||
}
|
||||
|
||||
private double GetDismissThreshold()
|
||||
{
|
||||
return Math.Max(DismissThresholdMin, ActualWidth * DismissThresholdRatio);
|
||||
}
|
||||
|
||||
private static bool IsDescendantOf(DependencyObject? descendant, DependencyObject ancestor)
|
||||
{
|
||||
while (descendant is not null)
|
||||
{
|
||||
if (ReferenceEquals(descendant, ancestor))
|
||||
return true;
|
||||
descendant = VisualTreeHelper.GetParent(descendant);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 进度条暂停与恢复
|
||||
|
||||
private void PauseProgress()
|
||||
{
|
||||
var currentWidth = ProgressBar.Width;
|
||||
ProgressBar.BeginAnimation(WidthProperty, null);
|
||||
ProgressBar.Width = currentWidth;
|
||||
}
|
||||
|
||||
private void ResumeProgress(double remainingMs)
|
||||
{
|
||||
if (remainingMs <= 0)
|
||||
return;
|
||||
var currentWidth = ProgressBar.Width;
|
||||
if (currentWidth <= 0)
|
||||
return;
|
||||
var anim = new DoubleAnimation(currentWidth, 0d, TimeSpan.FromMilliseconds(remainingMs));
|
||||
ProgressBar.BeginAnimation(WidthProperty, anim);
|
||||
}
|
||||
|
||||
private double GetProgressRemainingMs()
|
||||
{
|
||||
if (_progressStartWidth <= 0)
|
||||
return 0d;
|
||||
var currentWidth = ProgressBar.Width;
|
||||
return _progressTotalMs * (currentWidth / _progressStartWidth);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyVirtualizingElement<T> : FrameworkElement where T : FrameworkElement
|
||||
{
|
||||
private readonly Func<T> _initializer;
|
||||
|
||||
public MyVirtualizingElement(Func<T> initializer)
|
||||
{
|
||||
_initializer = initializer;
|
||||
this.EnableLazyLoad(() => Init());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化此控件。
|
||||
/// </summary>
|
||||
public T Init()
|
||||
{
|
||||
var element = _initializer();
|
||||
if (Parent is not null)
|
||||
{
|
||||
if (Parent is not Panel)
|
||||
throw new Exception("MyVirtualizingElement 的父级必须是一个 Panel");
|
||||
var parentPanel = (Panel)Parent;
|
||||
var currentIndex = parentPanel.Children.IndexOf(this);
|
||||
parentPanel.Children.RemoveAt(currentIndex);
|
||||
parentPanel.Children.Insert(currentIndex, element);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
public static implicit operator T(MyVirtualizingElement<T> virtualized)
|
||||
{
|
||||
return virtualized.Init();
|
||||
}
|
||||
}
|
||||
|
||||
// 非泛型形式
|
||||
public class MyVirtualizingElement : FrameworkElement
|
||||
{
|
||||
private readonly Func<FrameworkElement> _initializer;
|
||||
|
||||
public MyVirtualizingElement(Func<FrameworkElement> initializer)
|
||||
{
|
||||
_initializer = initializer;
|
||||
this.EnableLazyLoad(() => Init());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化此控件。
|
||||
/// </summary>
|
||||
public FrameworkElement Init()
|
||||
{
|
||||
var element = _initializer();
|
||||
if (Parent is not null)
|
||||
{
|
||||
if (Parent is not Panel)
|
||||
throw new Exception("MyVirtualizingElement 的父级必须是一个 Panel");
|
||||
var parentPanel = (Panel)Parent;
|
||||
var currentIndex = parentPanel.Children.IndexOf(this);
|
||||
parentPanel.Children.RemoveAt(currentIndex);
|
||||
parentPanel.Children.Insert(currentIndex, element);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实例化后的控件。
|
||||
/// 如果该控件没有实例化,则会立即实例化。
|
||||
/// 如果类型错误,则返回原值。
|
||||
/// </summary>
|
||||
public static FrameworkElement TryInit(FrameworkElement element)
|
||||
{
|
||||
if (typeof(MyVirtualizingElement<>).IsInstanceOfGenericType(element))
|
||||
{
|
||||
var method = element.GetType().GetMethod("Init", Type.EmptyTypes);
|
||||
return (FrameworkElement)method.Invoke(element, null);
|
||||
}
|
||||
return element is MyVirtualizingElement ? ((MyVirtualizingElement)element).Init() : element;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Core.UI.Controls.SvgIcon;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal static class SvgIconControlHelper
|
||||
{
|
||||
internal static bool HasSvgIcon(string? icon)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(icon);
|
||||
}
|
||||
|
||||
internal static void ApplyVisibility(
|
||||
Path legacyIcon,
|
||||
SvgIcon svgIcon,
|
||||
bool useSvgIcon)
|
||||
{
|
||||
legacyIcon.Visibility = useSvgIcon ? Visibility.Collapsed : Visibility.Visible;
|
||||
svgIcon.Visibility = useSvgIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
internal static void ApplyIcon(Path legacyIcon, SvgIcon svgIcon, string? svgIconName)
|
||||
{
|
||||
var useSvgIcon = HasSvgIcon(svgIconName);
|
||||
svgIcon.Icon = svgIconName ?? string.Empty;
|
||||
ApplyVisibility(legacyIcon, svgIcon, useSvgIcon);
|
||||
}
|
||||
|
||||
internal static void SetIconBrush(Path legacyIcon, SvgIcon svgIcon, bool useSvgIcon, Brush brush)
|
||||
{
|
||||
if (useSvgIcon)
|
||||
svgIcon.IconBrush = brush;
|
||||
else
|
||||
legacyIcon.Fill = brush;
|
||||
}
|
||||
|
||||
internal static void SetIconResource(Path legacyIcon, SvgIcon svgIcon, bool useSvgIcon, string resourceKey)
|
||||
{
|
||||
if (useSvgIcon)
|
||||
svgIcon.SetResourceReference(SvgIcon.IconBrushProperty, resourceKey);
|
||||
else
|
||||
legacyIcon.SetResourceReference(Shape.FillProperty, resourceKey);
|
||||
}
|
||||
|
||||
internal static void AnimateSvgIconBrushTo(
|
||||
SvgIcon svgIcon,
|
||||
string resourceKey,
|
||||
int duration,
|
||||
string? animationKey = null)
|
||||
{
|
||||
if (svgIcon.Visibility == Visibility.Visible)
|
||||
svgIcon.AnimateIconBrushTo(
|
||||
ResolveResourceColor(resourceKey),
|
||||
TimeSpan.FromMilliseconds(duration),
|
||||
animationKey: animationKey);
|
||||
}
|
||||
|
||||
internal static void AnimateSvgIconBrushTo(
|
||||
SvgIcon svgIcon,
|
||||
ModBase.MyColor color,
|
||||
int duration,
|
||||
string? animationKey = null)
|
||||
{
|
||||
if (svgIcon.Visibility == Visibility.Visible)
|
||||
svgIcon.AnimateIconBrushTo(
|
||||
new NColor((Color)color),
|
||||
TimeSpan.FromMilliseconds(duration),
|
||||
animationKey: animationKey);
|
||||
}
|
||||
|
||||
private static NColor ResolveResourceColor(string resourceKey)
|
||||
{
|
||||
if (!ThemeManager.AppResources.Contains(resourceKey))
|
||||
return new NColor(resourceKey);
|
||||
|
||||
var resource = ThemeManager.AppResources[resourceKey];
|
||||
return resource switch
|
||||
{
|
||||
SolidColorBrush brush => new NColor(brush),
|
||||
Color color => new NColor(color),
|
||||
Brush brush => new NColor(brush),
|
||||
_ => new NColor(resourceKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<Window
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
|
||||
mc:Ignorable="d"
|
||||
KeyDown="FormMain_KeyDown" MouseDown="FormMain_MouseDown" SizeChanged="FormMain_SizeChanged"
|
||||
Closing="FormMain_Closing" Activated="FormMain_Activated" Drop="FrmMain_Drop" StateChanged="WindowStateChanged"
|
||||
MouseMove="FormMain_MouseMove"
|
||||
x:Name="WindMain" x:Class="PCL.FormMain" Style="{StaticResource MyWindow}"
|
||||
AllowDrop="True" Topmost="True" Title="Plain Craft Launcher Community Edition " MinHeight="470" MinWidth="810"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico"
|
||||
ResizeMode="CanResize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500"
|
||||
WindowStartupLocation="CenterScreen" WindowStyle="SingleBorderWindow" AllowsTransparency="False">
|
||||
<Window.TaskbarItemInfo>
|
||||
<TaskbarItemInfo ThumbnailClipMargin="{Binding Margin, ElementName=PanForm}" />
|
||||
</Window.TaskbarItemInfo>
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome
|
||||
CaptionHeight="0"
|
||||
UseAeroCaptionButtons="False"
|
||||
ResizeBorderThickness="0"
|
||||
CornerRadius="12"
|
||||
GlassFrameThickness="0" />
|
||||
</WindowChrome.WindowChrome>
|
||||
<Grid SnapsToDevicePixels="True" UseLayoutRounding="True">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="60" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Rectangle Stroke="{x:Null}" StrokeThickness="0.0001" IsHitTestVisible="False" Height="10" VerticalAlignment="Top" Margin="16,0">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
|
||||
<GradientStop Color="#21000000" Offset="1" />
|
||||
<GradientStop Color="#11000000" Offset="0.7" />
|
||||
<GradientStop Color="#00000000" Offset="0" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle Stroke="{x:Null}" StrokeThickness="0.0001" IsHitTestVisible="False" Height="10" VerticalAlignment="Bottom" Margin="16,0">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
|
||||
<GradientStop Color="#21000000" Offset="0" />
|
||||
<GradientStop Color="#11000000" Offset="0.3" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Right" Margin="0,16" IsHitTestVisible="False" Stroke="{x:Null}" StrokeThickness="0.0001" Width="10">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
|
||||
<GradientStop Color="#21000000" Offset="0" />
|
||||
<GradientStop Color="#11000000" Offset="0.3" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Left" Stroke="{x:Null}" IsHitTestVisible="False" Width="10" Margin="0,16" StrokeThickness="0.0001">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
|
||||
<GradientStop Color="#21000000" Offset="1" />
|
||||
<GradientStop Color="#11000000" Offset="0.7" />
|
||||
<GradientStop Color="#00000000" Offset="0" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Left" Stroke="{x:Null}" IsHitTestVisible="False" Width="16" StrokeThickness="0.0001" VerticalAlignment="Top" Height="16">
|
||||
<Rectangle.Fill>
|
||||
<RadialGradientBrush Center="1,1" GradientOrigin="1,1" RadiusX="1" RadiusY="1">
|
||||
<GradientStop Color="#21000000" Offset="0.2" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Left" Stroke="{x:Null}" IsHitTestVisible="False" Width="16" StrokeThickness="0.0001" VerticalAlignment="Bottom" Height="16">
|
||||
<Rectangle.Fill>
|
||||
<RadialGradientBrush Center="1,0" GradientOrigin="1,0" RadiusX="1" RadiusY="1">
|
||||
<GradientStop Color="#21000000" Offset="0.2" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Right" Stroke="{x:Null}" IsHitTestVisible="False" Width="16" StrokeThickness="0.0001" VerticalAlignment="Bottom" Height="16">
|
||||
<Rectangle.Fill>
|
||||
<RadialGradientBrush Center="0,0" GradientOrigin="0,0" RadiusX="1" RadiusY="1">
|
||||
<GradientStop Color="#21000000" Offset="0.2" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle HorizontalAlignment="Right" Stroke="{x:Null}" IsHitTestVisible="False" Width="16" StrokeThickness="0.0001" VerticalAlignment="Top" Height="16">
|
||||
<Rectangle.Fill>
|
||||
<RadialGradientBrush Center="0,1" GradientOrigin="0,1" RadiusX="1" RadiusY="1">
|
||||
<GradientStop Color="#21000000" Offset="0.2" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Border x:Name="PanBack" Margin="10">
|
||||
<Border.Clip>
|
||||
<RectangleGeometry x:Name="RectForm" RadiusX="6" RadiusY="6" />
|
||||
</Border.Clip>
|
||||
<Grid x:Name="PanForm" SnapsToDevicePixels="False" UseLayoutRounding="False">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Canvas x:Name="ImgBack" IsHitTestVisible="False" Grid.Row="1"
|
||||
RenderOptions.BitmapScalingMode="LowQuality">
|
||||
<MediaElement x:Name="VideoBack" LoadedBehavior="Manual" UnloadedBehavior="Stop"
|
||||
Stretch="UniformToFill" IsHitTestVisible="False" Visibility="Visible"
|
||||
MediaEnded="VideoEnded" Volume="0" />
|
||||
</Canvas>
|
||||
<local:AnimatedBackgroundGrid Grid.Row="0" x:Name="PanTitle" Height="48"
|
||||
MouseLeftButtonDown="FormDragMove"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"
|
||||
BackgroundBrush="{DynamicResource ColorBrush3}">
|
||||
<!-- <Grid.Background> -->
|
||||
<!-- <LinearGradientBrush EndPoint="1,0" StartPoint="0,0"> -->
|
||||
<!-- <GradientStop Color="{StaticResource ColorObject4}" Offset="0"/> -->
|
||||
<!-- </LinearGradientBrush> -->
|
||||
<!-- </Grid.Background> -->
|
||||
<local:MyImage x:Name="ImgTitle" IsHitTestVisible="False" Stretch="UniformToFill" IsEnabled="False" RenderOptions.BitmapScalingMode="LowQuality" />
|
||||
<local:MyIconButton x:Name="BtnTitleClose" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,12,0" Theme="White" Click="BtnTitleClose_Click"
|
||||
SvgIcon="lucide/x" />
|
||||
<local:MyIconButton x:Name="BtnTitleMin" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,44,0" Theme="White" Click="BtnTitleMin_Click"
|
||||
SvgIcon="lucide/minus" />
|
||||
<local:MyIconButton x:Name="BtnTitleHelp" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,76,0" Theme="White" Visibility="Collapsed" Click="BtnTitleHelp_Click"
|
||||
SvgIcon="lucide/circle-help" />
|
||||
<Grid x:Name="PanTitleMain" HorizontalAlignment="Left" Width="{Binding ActualWidth, ElementName=PanTitle, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid x:Name="PanTitleLeft" SizeChanged="PanTitle_SizeChanged">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Path Grid.Column="0" x:Name="ShapeTitleLogo" Data="M26,29 v-25 h6 a7,7 180 0 1 0,14 h-6 M62.5,6.5 a10,11.5 180 1 0 0,18 M71,2.5 v24.5 h13.5" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="19,15.5,0,15.5" Stretch="fill" Stroke="White" StrokeThickness="2.2" Width="39" />
|
||||
<Path Grid.Column="0" x:Name="ShapeHMCLTitleLogo" Data="M 60.375 53.406 L 53.625 53.406 L 53.625 0.906 L 63.6 0.906 L 78.15 34.131 L 92.475 0.906 L 102.675 0.906 L 102.675 53.406 L 95.55 53.406 L 95.55 9.981 L 81.525 41.556 L 74.475 41.556 L 60.375 9.981 L 60.375 53.406 Z M 7.125 53.406 L 0 53.406 L 0 0.906 L 7.125 0.906 L 7.125 23.331 L 33 23.331 L 33 0.906 L 40.125 0.906 L 40.125 53.406 L 33 53.406 L 33 29.481 L 7.125 29.481 L 7.125 53.406 Z M 149.85 2.706 L 147.675 8.706 Q 145.35 7.581 142.913 6.906 Q 140.475 6.231 137.175 6.231 Q 132.375 6.231 128.588 8.706 Q 124.8 11.181 122.625 15.831 A 25.755 25.755 0 0 0 120.458 26.263 A 38.963 38.963 0 0 0 120.45 27.081 Q 120.45 33.381 122.588 38.106 Q 124.725 42.831 128.512 45.456 Q 132.3 48.081 137.175 48.081 Q 140.85 48.081 143.55 47.181 Q 146.25 46.281 148.8 44.856 L 150.975 50.781 Q 148.2 52.281 144.638 53.293 Q 141.075 54.306 136.35 54.306 Q 129.525 54.306 124.275 51.043 Q 119.025 47.781 116.1 41.743 A 31.603 31.603 0 0 1 113.201 28.922 A 45.601 45.601 0 0 1 113.175 27.381 Q 113.175 19.356 116.138 13.168 Q 119.1 6.981 124.538 3.493 Q 129.975 0.006 137.25 0.006 Q 141 0.006 144.075 0.718 Q 147.15 1.431 149.85 2.706 Z M 191.925 53.406 L 160.725 53.406 L 160.725 0.906 L 167.85 0.906 L 167.85 47.031 L 191.925 47.031 L 191.925 53.406 Z"
|
||||
StrokeThickness="0.2" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Height="15" Margin="45,15.5,0,15.5" Stretch="Fill" Stroke="White" Fill="White" Width="52" />
|
||||
<TextBlock Grid.Column="0" x:Name="LabTitleLogo" Visibility="Collapsed" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="18,0,0,0" Foreground="White" FontSize="17" VerticalAlignment="Center" />
|
||||
<local:MyImage Grid.Column="0" x:Name="ImageTitleLogo" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="7" VerticalAlignment="Center" Visibility="Collapsed" />
|
||||
<Border x:Name="CELogo" Grid.Column="1" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="10,0,0,0" Background="White" CornerRadius="5" Height="18" Width="20">
|
||||
<Path Fill="{DynamicResource ColorBrush3}" Data="M3.1709 11.6098c-0.0423,-0.1524 -0.0783,-0.2985 -0.1079,-0.4382 -0.0297,-0.1397 -0.0551,-0.3048 -0.0762,-0.4953 -0.0212,-0.1905 -0.036,-0.4212 -0.0445,-0.6921 -0.0085,-0.2709 -0.0127,-0.6011 -0.0127,-0.9906 0,-0.3979 0.0042,-0.7324 0.0127,-1.0033 0.0085,-0.2709 0.0233,-0.5017 0.0445,-0.6922 0.0211,-0.1905 0.0465,-0.3577 0.0762,-0.5016 0.0296,-0.1439 0.0656,-0.2836 0.1079,-0.4191 0.2286,-0.7281 0.6054,-1.2658 1.1303,-1.6129 0.5249,-0.3471 1.1684,-0.5207 1.9304,-0.5207 0.6943,0 1.2954,0.1587 1.8034,0.4762 0.508,0.3175 0.8932,0.7684 1.1557,1.3526 0,0 -0.0169,0.0212 -0.0508,0.0635l-0.8001 0.381c-0.0169,0.0169 -0.0381,0.0127 -0.0635,-0.0127 -0.1947,-0.3979 -0.453,-0.7176 -0.7747,-0.9589 -0.3217,-0.2413 -0.7451,-0.3619 -1.27,-0.3619 -0.5334,0 -0.9673,0.1249 -1.3017,0.3746 -0.3345,0.2498 -0.5906,0.6118 -0.7684,1.0859 -0.0254,0.1016 -0.0508,0.2117 -0.0762,0.3302 -0.0254,0.1185 -0.0444,0.2625 -0.0571,0.4318 -0.0127,0.1693 -0.0233,0.3831 -0.0318,0.6413 -0.0085,0.2583 -0.0127,0.5737 -0.0127,0.9462 0,0.3725 0.0042,0.6858 0.0127,0.9398 0.0085,0.254 0.0191,0.4657 0.0318,0.635 0.0127,0.1693 0.0317,0.3154 0.0571,0.4381 0.0254,0.1228 0.0508,0.2308 0.0762,0.3239 0.1778,0.4741 0.4339,0.8361 0.7684,1.0858 0.3344,0.2498 0.7683,0.3747 1.3017,0.3747 0.5249,0 0.9483,-0.1207 1.27,-0.362 0.3217,-0.2413 0.58,-0.5609 0.7747,-0.9588 0.0254,-0.0254 0.0466,-0.0296 0.0635,-0.0127l0.8001 0.3937c0,0 0.0169,0.0212 0.0508,0.0635 -0.2625,0.5842 -0.6477,1.035 -1.1557,1.3525 -0.508,0.3175 -1.1091,0.4763 -1.8034,0.4763 -0.762,0 -1.4055,-0.1757 -1.9304,-0.5271 -0.5249,-0.3513 -0.9017,-0.8868 -1.1303,-1.6065 M11.4792 4.4343c0,-0.0254 0.0127,-0.0381 0.0381,-0.0381l5.715 0c0.0254,0 0.0381,0.0127 0.0381,0.0381l0 0.8763c0,0.0254 -0.0127,0.0381 -0.0381,0.0381l-4.5339 0c-0.1185,0 -0.1778,0.0635 -0.1778,0.1905l0 2.794c0,0.1185 0.0593,0.1778 0.1778,0.1778l3.8481 0c0.0254,0 0.0381,0.0127 0.0381,0.0381l0 0.8763c0,0.0254 -0.0127,0.0381 -0.0381,0.0381l-3.8481 0c-0.1185,0 -0.1778,0.0635 -0.1778,0.1905l0 2.8956c0,0.1185 0.0593,0.1778 0.1778,0.1778l4.5339 0c0.0254,0 0.0381,0.0127 0.0381,0.0381l0 0.889c0,0.0254 -0.0127,0.0381 -0.0381,0.0381l-5.715 0c-0.0254,0 -0.0381,-0.0127 -0.0381,-0.0381l0 -9.2202z" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" x:Name="PanTitleSelect" HorizontalAlignment="Left" Height="27"
|
||||
VerticalAlignment="Center" Orientation="Horizontal" Margin="13,0">
|
||||
<local:MyRadioButton x:Name="BtnTitleSelect0" Text="{DynamicResource Main.Tab.Launch}" Tag="0" Margin="5,0" Checked="True"
|
||||
Padding="2,0" Check="BtnTitleSelect_Click"
|
||||
LogoScale="0.9"
|
||||
SvgIcon="lucide/play" />
|
||||
<local:MyRadioButton x:Name="BtnTitleSelect1" Text="{DynamicResource Main.Tab.Download}" Tag="1" Margin="5,0" Padding="2,0"
|
||||
Check="BtnTitleSelect_Click"
|
||||
LogoScale="0.9"
|
||||
SvgIcon="lucide/download" />
|
||||
<local:MyRadioButton x:Name="BtnTitleSelect2" Text="{DynamicResource Main.Tab.Settings}" Tag="2" Margin="5,0" Padding="2,0"
|
||||
Check="BtnTitleSelect_Click"
|
||||
LogoScale="1.1"
|
||||
SvgIcon="lucide/settings" />
|
||||
<local:MyRadioButton x:Name="BtnTitleSelect3" Text="{DynamicResource Main.Tab.Tools}" Tag="3" Margin="5,0" Padding="2,0"
|
||||
Check="BtnTitleSelect_Click"
|
||||
LogoScale="1"
|
||||
SvgIcon="lucide/wrench" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Grid x:Name="PanTitleInner" Visibility="Collapsed" Margin="-16,0,0,0" Opacity="0"
|
||||
HorizontalAlignment="Left" Width="{Binding ActualWidth, ElementName=PanTitle, Mode=OneWay}">
|
||||
<local:MyIconButton HorizontalAlignment="Left" Theme="White" x:Name="BtnTitleInner"
|
||||
Margin="12,0,0,0" Height="28" Width="28" Click="BtnTitleInner_Click"
|
||||
LogoScale="0.87"
|
||||
SvgIcon="lucide/arrow-left" />
|
||||
<TextBlock SnapsToDevicePixels="False" IsHitTestVisible="False" Name="LabTitleInner"
|
||||
VerticalAlignment="Center" Margin="47,1,60,0" HorizontalAlignment="Left"
|
||||
Foreground="White" FontSize="15" />
|
||||
</Grid>
|
||||
</local:AnimatedBackgroundGrid>
|
||||
<Grid x:Name="PanLeft" Grid.Row="1" HorizontalAlignment="Left" SnapsToDevicePixels="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:BlurBorder IsHitTestVisible="False">
|
||||
<local:AnimatedBackgroundGrid x:Name="RectLeftBackground" Width="0"
|
||||
BackgroundBrush="{DynamicResource ColorBrushTransparentBackground}" />
|
||||
</controls:BlurBorder>
|
||||
<Rectangle x:Name="RectLeftShadow" Width="4" HorizontalAlignment="Left" IsHitTestVisible="False"
|
||||
Grid.Column="1">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0" Opacity="0.04">
|
||||
<GradientStop Color="#000000" Offset="0" />
|
||||
<GradientStop Color="#00000000" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</Grid>
|
||||
<Grid x:Name="PanMain" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="1" Grid.IsSharedSizeScope="True" UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True">
|
||||
<Border x:Name="PanMainRight" RenderOptions.BitmapScalingMode="NearestNeighbor"
|
||||
UseLayoutRounding="False" SnapsToDevicePixels="False" />
|
||||
</Border>
|
||||
<Border Grid.Column="0" Grid.IsSharedSizeScope="True" UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True">
|
||||
<Border x:Name="PanMainLeft" RenderOptions.BitmapScalingMode="NearestNeighbor"
|
||||
SizeChanged="PanMainLeft_SizeChanged"
|
||||
UseLayoutRounding="False" SnapsToDevicePixels="False" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<StackPanel x:Name="PanHint" UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True" HorizontalAlignment="Left" VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,20"
|
||||
Grid.Row="0" Grid.RowSpan="2" Panel.ZIndex="1" />
|
||||
<StackPanel x:Name="PanExtraButtons" HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="1" Margin="15">
|
||||
<local:MyExtraButton x:Name="BtnExtraUpdateRestart" HorizontalAlignment="Right"
|
||||
Click="BtnExtraUpdateRestart_Click"
|
||||
VerticalAlignment="Center" ToolTip="{DynamicResource Main.Extra.UpdateRestart.ToolTip}"
|
||||
Visibility="Collapsed"
|
||||
SvgIcon="lucide/refresh-cw" />
|
||||
<local:MyExtraButton x:Name="BtnExtraBack" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Click="BtnExtraBack_Click"
|
||||
ToolTip="{DynamicResource Main.Extra.BackToTop.ToolTip}" Visibility="Collapsed"
|
||||
SvgIcon="lucide/arrow-up-to-line" />
|
||||
<local:MyExtraButton x:Name="BtnExtraDownload" HorizontalAlignment="Right"
|
||||
Click="BtnExtraDownload_Click"
|
||||
VerticalAlignment="Center" ToolTip="{DynamicResource Main.Extra.TaskManager.ToolTip}" Visibility="Collapsed"
|
||||
SvgIcon="lucide/download" />
|
||||
<local:MyExtraButton x:Name="BtnExtraApril" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Click="BtnExtraApril_Click"
|
||||
ToolTip="{DynamicResource Main.Extra.GiveUp.ToolTip}" Visibility="Collapsed"
|
||||
SvgIcon="lucide/flag" />
|
||||
<local:MyExtraButton x:Name="BtnExtraShutdown" HorizontalAlignment="Right"
|
||||
Click="BtnExtraShutdown_Click"
|
||||
VerticalAlignment="Center" ToolTip="{DynamicResource Main.Extra.ShutdownMinecraft.ToolTip}" Visibility="Collapsed"
|
||||
SvgIcon="lucide/power" />
|
||||
<local:MyExtraButton x:Name="BtnExtraLog" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Click="BtnExtraLog_Click"
|
||||
ToolTip="{DynamicResource Main.Extra.GameLog.ToolTip}" Visibility="Collapsed"
|
||||
SvgIcon="lucide/scroll-text" />
|
||||
<local:MyExtraButton x:Name="BtnExtraMusic" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Click="BtnExtraMusic_Click" RightClick="BtnExtraMusic_RightClick"
|
||||
Visibility="Collapsed" CanRightClick="True" />
|
||||
</StackPanel>
|
||||
<controls:BlurBorder x:Name="PanMsgBackground" Grid.Row="0" Grid.RowSpan="2" Background="#00000000"
|
||||
Panel.ZIndex="2" Visibility="Collapsed">
|
||||
<Grid x:Name="PanMsg" Background="#00000000" MouseLeftButtonDown="FormDragMove" />
|
||||
</controls:BlurBorder>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,2245 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Minecraft;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Core.UI.Theme;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Core.Utils.Validate;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class FormMain
|
||||
{
|
||||
// 愚人节鼠标位置
|
||||
public MouseEventArgs lastMouseArg;
|
||||
|
||||
private void FormMain_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
lastMouseArg = e;
|
||||
}
|
||||
|
||||
#region 基础
|
||||
|
||||
// 更新日志
|
||||
private void ShowUpdateLog()
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
var changelogFile = $"{ModBase.pathTemp}CEUpdateLog.md";
|
||||
string changelog;
|
||||
if (File.Exists(changelogFile))
|
||||
changelog = ModBase.ReadFile(changelogFile);
|
||||
else
|
||||
changelog = Lang.Text("Main.UpdateLog.Empty");
|
||||
if (ModMain.MyMsgBoxMarkdown(changelog,
|
||||
Lang.Text("Main.UpdateLog.Title", ModBase.versionBranchName, ModBase.versionBaseName), Lang.Text("Common.Action.Confirm"), Lang.Text("Main.UpdateLog.FullChangelog")) ==
|
||||
2) ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases");
|
||||
}, "UpdateLog Output");
|
||||
}
|
||||
|
||||
// 窗口加载
|
||||
private bool isWindowLoadFinished;
|
||||
private readonly DragHelper _helper = new();
|
||||
|
||||
public FormMain()
|
||||
{
|
||||
ModBase.applicationStartTick = TimeUtils.GetTimeTick();
|
||||
// 刷新主题
|
||||
// ThemeCheckAll(False)
|
||||
// ThemeRefreshColor()
|
||||
ThemeService.ColorModeChanged += (_, _) => ThemeManager.ThemeRefresh();
|
||||
ThemeService.ColorThemeChanged += theme => ThemeManager.ThemeRefresh((int)theme);
|
||||
// 窗体参数初始化
|
||||
ModMain.frmMain = this;
|
||||
ModMain.frmLaunchLeft = new PageLaunchLeft();
|
||||
ModMain.frmLaunchRight = new PageLaunchRight();
|
||||
// 版本号改变
|
||||
var lastVersion = States.System.LastVersion;
|
||||
if (lastVersion < ModBase.versionCode)
|
||||
{
|
||||
// 重新询问是否启用遥测数据收集
|
||||
if (lastVersion <= 511)
|
||||
{
|
||||
if (!Config.System.TelemetryConfig.IsDefault() && Config.System.Telemetry)
|
||||
{
|
||||
Config.System.TelemetryConfig.Reset();
|
||||
ModBase.Log("[Start] 遥测策略变更:由旧版本升级到含新版遥测的版本,已重置遥测设置");
|
||||
}
|
||||
}
|
||||
// 触发升级
|
||||
UpgradeSub(lastVersion);
|
||||
}
|
||||
else if (lastVersion > ModBase.versionCode)
|
||||
// 触发降级
|
||||
DowngradeSub(lastVersion);
|
||||
|
||||
_ = Config.Preference.Theme.ThemeSelected;
|
||||
// 注册拖拽事件(不能直接加 Handles,否则没用;#6340)
|
||||
AddHandler(DragDrop.DragEnterEvent, new DragEventHandler(HandleDrag), true);
|
||||
AddHandler(DragDrop.DragOverEvent, new DragEventHandler(HandleDrag), true);
|
||||
// 注册 MsgBox 事件
|
||||
MsgBoxWrapper.OnShow += ModMain.MsgBoxWrapper_OnShow;
|
||||
// 注册 Hint 事件
|
||||
HintWrapper.OnShow += HintService.HintWrapper_OnShow;
|
||||
// 加载 UI
|
||||
InitializeComponent();
|
||||
Opacity = 0d;
|
||||
try
|
||||
{
|
||||
Height = States.UI.WindowHeight;
|
||||
Width = States.UI.WindowWidth;
|
||||
}
|
||||
catch (Exception ex) // 修复 #2019
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"读取窗口默认大小失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
Height = MinHeight + 100d;
|
||||
Width = MinWidth + 100d;
|
||||
}
|
||||
|
||||
// 管理员权限下文件拖拽
|
||||
if (ProcessInterop.IsAdmin())
|
||||
{
|
||||
ModBase.Log("[Start] PCL 当前正以管理员权限运行");
|
||||
SourceInitialized += (_, _) =>
|
||||
{
|
||||
var windowInterop = new WindowInteropHelper(this);
|
||||
_helper.HwndSource = HwndSource.FromHwnd(windowInterop.Handle);
|
||||
_helper.AddHook();
|
||||
};
|
||||
Closing += (_, _) => _helper.RemoveHook();
|
||||
_helper.DragDrop += (_, _) => FileDrag(_helper.DropFilePaths);
|
||||
}
|
||||
|
||||
if (ModMain.frmLaunchLeft.Parent is not null)
|
||||
ModMain.frmLaunchLeft.SetValue(ContentPresenter.ContentProperty, null);
|
||||
if (ModMain.frmLaunchRight.Parent is not null)
|
||||
ModMain.frmLaunchRight.SetValue(ContentPresenter.ContentProperty, null);
|
||||
PanMainLeft.Child = ModMain.frmLaunchLeft;
|
||||
pageLeft = ModMain.frmLaunchLeft;
|
||||
PanMainRight.Child = ModMain.frmLaunchRight;
|
||||
pageRight = ModMain.frmLaunchRight;
|
||||
ModMain.frmLaunchRight.PageState = MyPageRight.PageStates.ContentStay;
|
||||
// 调试模式提醒
|
||||
if (ModBase.modeDebug)
|
||||
HintService.Hint(Lang.Text("Main.DebugMode.Hint"));
|
||||
// 尽早执行的加载池
|
||||
ModFolder.mcFolderListLoader
|
||||
.Start(0); // 为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid
|
||||
|
||||
ModBase.Log("[Start] 第二阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.applicationStartTick) + " ms");
|
||||
// 注册生命周期状态事件
|
||||
Lifecycle.When(LifecycleState.WindowCreated, FormMain_Loaded);
|
||||
}
|
||||
|
||||
private void FormMain_Loaded() // (sender As Object, e As RoutedEventArgs) Handles Me.Loaded
|
||||
{
|
||||
FormMain_SizeChanged();
|
||||
ModBase.applicationStartTick = TimeUtils.GetTimeTick();
|
||||
ModBase.frmHandle = new WindowInteropHelper(this).Handle;
|
||||
// 读取设置
|
||||
PageSetupUI.BackgroundRefresh(false, true);
|
||||
ModMusic.MusicRefreshPlay(false, true);
|
||||
// 扩展按钮
|
||||
BtnExtraUpdateRestart.showCheck = BtnExtraUpdateRestart_ShowCheck;
|
||||
BtnExtraDownload.showCheck = BtnExtraDownload_ShowCheck;
|
||||
BtnExtraBack.showCheck = BtnExtraBack_ShowCheck;
|
||||
BtnExtraApril.showCheck = BtnExtraApril_ShowCheck;
|
||||
BtnExtraShutdown.showCheck = BtnExtraShutdown_ShowCheck;
|
||||
BtnExtraLog.showCheck = BtnExtraLog_ShowCheck;
|
||||
BtnExtraApril.ShowRefresh();
|
||||
// 初始化尺寸改变
|
||||
if (!Config.Preference.LockWindowSize)
|
||||
AddResizer();
|
||||
else
|
||||
RemoveResizer();
|
||||
// PLC 彩蛋
|
||||
if (RandomUtils.NextInt(1, 1000) == 233)
|
||||
ShapeTitleLogo.Data = (Geometry)new GeometryConverter().ConvertFromString(
|
||||
"M26,29 v-25 h6 a7,7 180 0 1 0,14 h-6 M83,6.5 a10,11.5 180 1 0 0,18 M48,2.5 v24.5 h13.5");
|
||||
// 加载窗口
|
||||
|
||||
ThemeManager.ThemeRefresh();
|
||||
ModSetup.ApplyAll();
|
||||
Lifecycle.CurrentApplication.Resources["BlurSamplingRate"] = Config.Preference.Blur.SamplingRate * 0.01d;
|
||||
Lifecycle.CurrentApplication.Resources["BlurType"] = Config.Preference.Blur.KernelType;
|
||||
if (Config.Preference.Blur.IsEnabled)
|
||||
Lifecycle.CurrentApplication.Resources["BlurRadius"] = Config.Preference.Blur.Radius * 1.0d;
|
||||
else
|
||||
Lifecycle.CurrentApplication.Resources["BlurRadius"] = 0.0d;
|
||||
|
||||
// #If DEBUG Then
|
||||
// MinHeight = 50
|
||||
// MinWidth = 50
|
||||
// #End If
|
||||
Topmost = false;
|
||||
if (ModMain.frmStart is not null)
|
||||
ModMain.frmStart.Close(new TimeSpan(0, 0, 0, 0, (int)Math.Round(400d / ModAnimation.aniSpeed)));
|
||||
// 更改窗口
|
||||
// Top = (GetWPFSize(My.Computer.Screen.WorkingArea.Height) - Height) / 2
|
||||
// Left = (GetWPFSize(My.Computer.Screen.WorkingArea.Width) - Width) / 2
|
||||
isSizeSaveable = true;
|
||||
ShowWindowToTop();
|
||||
var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
|
||||
hwndSource.AddHook(WndProc);
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() => ModAnimation.AniControlEnabled -= 1, 50),
|
||||
ModAnimation.AaOpacity(this, Config.Preference.Theme.WindowOpacity / 1000d + 0.4d, 250, 100),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, -TransformPos.Y, 600,
|
||||
100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
RenderTransform = null;
|
||||
isWindowLoadFinished = true;
|
||||
ModBase.Log(
|
||||
$"[System] DPI:{ModBase.dpi},系统版本:{Environment.OSVersion.VersionString},PCL 位置:{Basics.ExecutablePath}");
|
||||
}, after: true)
|
||||
}, "Form Show");
|
||||
// Timer 启动
|
||||
ModAnimation.AniStart();
|
||||
ModMain.TimerMainStart();
|
||||
// 特殊版本提示
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
// 特殊版本提示
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
#if DEBUG || DEBUGCI
|
||||
|
||||
if (Environment.GetEnvironmentVariable("PCL_DISABLE_DEBUG_HINT") is null)
|
||||
{
|
||||
|
||||
#if DEBUG
|
||||
var hint = Lang.Text("Main.SpecialVersion.DebugHint");
|
||||
#else
|
||||
var hint = Lang.Text("Main.SpecialVersion.CiHint");
|
||||
#endif
|
||||
|
||||
ModMain.MyMsgBox(
|
||||
$"{hint}{"\r\n"}{"\r\n"}{Lang.Text("Main.SpecialVersion.HideHintNotice")}",
|
||||
Lang.Text("Main.SpecialVersion.Title"), Lang.Text("Main.SpecialVersion.IUnderstand"), Lang.Text("Main.SpecialVersion.OpenDownloadPageAndExit"), isWarn: true, button2Action: () =>
|
||||
{
|
||||
ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/latest");
|
||||
EndProgram(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
// EULA 提示
|
||||
if (!States.System.LauncherEula)
|
||||
switch (ModMain.MyMsgBox(Lang.Text("Main.Eula.Message"), Lang.Text("Main.Eula.Title"), Lang.Text("Common.Action.Agree"), Lang.Text("Common.Action.Decline"), Lang.Text("Main.Eula.View"),
|
||||
button3Action: () => ModBase.OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW")))
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
States.System.LauncherEula = true;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
EndProgram(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 遥测提示
|
||||
if (Config.System.TelemetryConfig.IsDefault())
|
||||
{
|
||||
var selection = ModMain.MyMsgBox(
|
||||
Lang.Text("Main.Telemetry.Message"),
|
||||
Lang.Text("Main.Telemetry.Title"), Lang.Text("Common.Action.Agree"), Lang.Text("Common.Action.Decline"));
|
||||
Config.System.TelemetryConfig.SetValue(selection == 1, forceNewValue: true);
|
||||
}
|
||||
// 启动加载器池
|
||||
try
|
||||
{
|
||||
ModDownload.dlClientListMojangLoader.Start(1); // PCL 会同时根据这里的加载结果决定是否使用官方源进行下载
|
||||
RunCountSub();
|
||||
UpdateManager.serverLoader.Start(1);
|
||||
ModBase.RunInNewThread(ModMain.TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"初始化加载池运行失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
HardwareInfo.GetHardwareInfo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"初始弹窗提示运行失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
}, "Start Loader", ThreadPriority.BelowNormal);
|
||||
|
||||
ModBase.Log($"[Start] 第三阶段加载用时:{TimeUtils.GetTimeTick() - ModBase.applicationStartTick} ms");
|
||||
}
|
||||
|
||||
// 根据打开次数触发的事件
|
||||
private void RunCountSub()
|
||||
{
|
||||
States.System.StartupCount += 1;
|
||||
}
|
||||
|
||||
// 升级与降级事件
|
||||
private void UpgradeSub(int lastVersionCode)
|
||||
{
|
||||
ModBase.Log("[Start] 版本号从 " + lastVersionCode + " 升高到 " + ModBase.versionCode);
|
||||
States.System.LastVersion = ModBase.versionCode;
|
||||
// 检查有记录的最高版本号
|
||||
int lowerVersionCode;
|
||||
#if BETA
|
||||
lowerVersionCode = States.System.LastBetaVersion;
|
||||
if (lowerVersionCode < ModBase.versionCode)
|
||||
{
|
||||
States.System.LastBetaVersion = ModBase.versionCode;
|
||||
ModBase.Log($"[Start] 最高版本号从 {lowerVersionCode} 升高到 {ModBase.versionCode}");
|
||||
}
|
||||
#else
|
||||
lowerVersionCode = States.System.LastAlphaVersion;
|
||||
if (lowerVersionCode < ModBase.versionCode)
|
||||
{
|
||||
States.System.LastAlphaVersion = ModBase.versionCode;
|
||||
ModBase.Log($"[Start] 最高版本号从 {lowerVersionCode} 升高到 {ModBase.versionCode}");
|
||||
}
|
||||
#endif
|
||||
// 被移除的窗口设置选项 (Commit 3161488 2026/1/23)
|
||||
if ((int)Config.Launch.GameWindowMode == 5)
|
||||
Config.Launch.GameWindowMode = GameWindowSizeMode.Default;
|
||||
|
||||
// 更新后展示社区版提示
|
||||
UpdateManager.ShowCEAnnounce();
|
||||
// 输出更新日志
|
||||
if (lastVersionCode <= 0)
|
||||
return;
|
||||
if (lowerVersionCode >= ModBase.versionCode)
|
||||
return;
|
||||
ShowUpdateLog();
|
||||
|
||||
// 重置自定义主页配置
|
||||
if (lastVersionCode < 521 && Config.Preference.Homepage.SelectedPreset >= 3)
|
||||
{
|
||||
Config.Preference.Homepage.SelectedPreset = 0;
|
||||
ModMain.MyMsgBox(Lang.Text("Main.HomepageReset.Content"), Lang.Text("Main.HomepageReset.Title"));
|
||||
}
|
||||
}
|
||||
|
||||
private void DowngradeSub(int lastVersionCode)
|
||||
{
|
||||
ModBase.Log("[Start] 版本号从 " + lastVersionCode + " 降低到 " + ModBase.versionCode);
|
||||
States.System.LastVersion = ModBase.versionCode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 自定义窗口
|
||||
|
||||
private bool canResize = true;
|
||||
|
||||
// 重写窗口边缘判定以使 DWM 自带的 resizer 行为看起来比较正常
|
||||
private nint _SizeWndProc(nint hWnd, int msg, nint wParam, nint lParam, ref bool handled)
|
||||
{
|
||||
// 窗口活动常量
|
||||
const int WM_NCHITTEST = 0x84;
|
||||
const int HTCLIENT = 1;
|
||||
const int HTLEFT = 10;
|
||||
const int HTRIGHT = 11;
|
||||
const int HTTOP = 12;
|
||||
const int HTTOPLEFT = 13;
|
||||
const int HTTOPRIGHT = 14;
|
||||
const int HTBOTTOM = 15;
|
||||
const int HTBOTTOMLEFT = 16;
|
||||
const int HTBOTTOMRIGHT = 17;
|
||||
|
||||
// WPF 尺寸的 offset
|
||||
const int offsetWpf = 6;
|
||||
const int hitWidthWpf = 5;
|
||||
|
||||
// 过滤非 WM_NCHITTEST 事件
|
||||
if (msg != WM_NCHITTEST)
|
||||
return nint.Zero;
|
||||
|
||||
// 提取鼠标坐标
|
||||
// 没妈的 VB 强转还得检查一下幻想的妈是不是还活着
|
||||
var mouseBytes = BitConverter.GetBytes(lParam.ToInt64());
|
||||
var xMouse = BitConverter.ToInt16(mouseBytes, 0);
|
||||
var yMouse = BitConverter.ToInt16(mouseBytes, 2);
|
||||
|
||||
// 获取窗口参数
|
||||
var windowRect = WindowInterop.GetWindowRectangle(hWnd);
|
||||
var windowBounds = windowRect.ToWindowBounds();
|
||||
|
||||
// 判断鼠标是否在窗口范围内
|
||||
var isInWindow = xMouse >= windowRect.Left && xMouse <= windowRect.Right && yMouse >= windowRect.Top &&
|
||||
yMouse <= windowRect.Bottom;
|
||||
|
||||
// 过滤不在窗口内的请求
|
||||
if (!isInWindow)
|
||||
return nint.Zero;
|
||||
|
||||
// 如果 CanResize 为 False,直接返回 HTCLIENT
|
||||
if (!canResize)
|
||||
return new nint(HTCLIENT);
|
||||
|
||||
// 真实像素尺寸的 offset
|
||||
var dpi = VisualTreeHelper.GetDpi(this);
|
||||
var offsetPxX = offsetWpf * dpi.DpiScaleX;
|
||||
var offsetPxY = offsetWpf * dpi.DpiScaleY;
|
||||
var hitWidthPxX = hitWidthWpf * dpi.DpiScaleX;
|
||||
var hitWidthPxY = hitWidthWpf * dpi.DpiScaleY;
|
||||
|
||||
// 计算鼠标相对于窗口左上角的物理像素位置
|
||||
var relX = xMouse - windowRect.Left;
|
||||
var relY = yMouse - windowRect.Top;
|
||||
var w = windowBounds.Width;
|
||||
var h = windowBounds.Height;
|
||||
|
||||
// 判定是否命中偏移后的热区
|
||||
var inLeft = relX >= offsetPxX && relX <= offsetPxX + hitWidthPxX;
|
||||
var inRight = relX <= w - offsetPxX && relX >= w - offsetPxX - hitWidthPxX;
|
||||
var inTop = relY >= offsetPxY && relY <= offsetPxY + hitWidthPxY;
|
||||
var inBottom = relY <= h - offsetPxY && relY >= h - offsetPxY - hitWidthPxY;
|
||||
|
||||
handled = true; // 接管该区域的消息
|
||||
|
||||
// 返回结果
|
||||
if (inTop && inLeft)
|
||||
return new nint(HTTOPLEFT);
|
||||
if (inTop && inRight)
|
||||
return new nint(HTTOPRIGHT);
|
||||
if (inBottom && inLeft)
|
||||
return new nint(HTBOTTOMLEFT);
|
||||
if (inBottom && inRight)
|
||||
return new nint(HTBOTTOMRIGHT);
|
||||
if (inLeft)
|
||||
return new nint(HTLEFT);
|
||||
if (inRight)
|
||||
return new nint(HTRIGHT);
|
||||
if (inTop)
|
||||
return new nint(HTTOP);
|
||||
if (inBottom)
|
||||
return new nint(HTBOTTOM);
|
||||
|
||||
// 如果在 0-offset 范围内,返回 HTCLIENT 杀掉默认缩放
|
||||
return new nint(HTCLIENT);
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
// 硬件加速
|
||||
if (Config.System.DisableHardwareAcceleration)
|
||||
{
|
||||
var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
|
||||
if (hwndSource is not null) hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly;
|
||||
}
|
||||
|
||||
base.OnSourceInitialized(e);
|
||||
|
||||
// 获取当前窗口句柄
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var source = HwndSource.FromHwnd(hwnd);
|
||||
if (source is not null)
|
||||
{
|
||||
// 渲染层允许 Alpha 通道通过
|
||||
source.CompositionTarget.BackgroundColor = Colors.Transparent;
|
||||
// 魔改窗口边缘判定
|
||||
source.AddHook(_SizeWndProc);
|
||||
}
|
||||
|
||||
// 设置 DWM 窗口框架
|
||||
try
|
||||
{
|
||||
WindowInterop.ExtendFrameIntoClientArea(hwnd, -1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error("DWM 窗口框架应用失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭
|
||||
private void FormMain_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
EndProgram(true);
|
||||
e.Cancel = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正常关闭程序。程序将在执行此方法后约 0.3s 退出。
|
||||
/// </summary>
|
||||
/// <param name="sendWarning">是否在还有下载任务未完成时发出警告。</param>
|
||||
/// <param name="isUpdating">是否正在更新重启</param>
|
||||
public void EndProgram(bool sendWarning, bool isUpdating = false)
|
||||
{
|
||||
// 发出警告
|
||||
if (sendWarning && ModNet.HasDownloadingTask())
|
||||
{
|
||||
if (ModMain.MyMsgBox(Lang.Text("Main.Exit.HasDownloadingTask"), Lang.Text("Common.Dialog.Title"), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
// 强行结束下载任务
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
ModBase.Log("[System] 正在强行停止任务");
|
||||
foreach (var Task in ModLoader.loaderTaskbar.ToList())
|
||||
Task.Abort();
|
||||
}, "强行停止下载任务");
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭联机大厅
|
||||
// Await LobbyController.CloseAsync().ConfigureAwait(False)
|
||||
// 存储上次使用的档案编号
|
||||
ModProfile.SaveProfile();
|
||||
// 关闭
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
// 清理视频背景
|
||||
VideoBack.Stop();
|
||||
VideoBack.Source = null;
|
||||
VideoBack.Close();
|
||||
IsHitTestVisible = false;
|
||||
if (RenderTransform is null)
|
||||
{
|
||||
var transformPos = new TranslateTransform(0d, 0d);
|
||||
var transformRotate = new RotateTransform(0d);
|
||||
var transformScale = new ScaleTransform(1d, 1d);
|
||||
transformScale.CenterX = Width / 2d;
|
||||
transformScale.CenterY = Height / 2d;
|
||||
RenderTransform = new TransformGroup
|
||||
{ Children = new TransformCollection([transformRotate, transformPos, transformScale]) };
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, -Opacity, 140, 40,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i =>
|
||||
{
|
||||
transformScale.ScaleX += (double)i;
|
||||
transformScale.ScaleY += (double)i;
|
||||
}, 0.88d - transformScale.ScaleX, 180),
|
||||
ModAnimation.AaDouble(i => transformPos.Y += (double)i,
|
||||
20d - transformPos.Y, 180, 0,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => transformRotate.Angle += (double)i,
|
||||
0.6d - transformRotate.Angle, 180, 0,
|
||||
new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
Visibility = Visibility.Collapsed;
|
||||
ShowInTaskbar = false;
|
||||
}, 210),
|
||||
ModAnimation.AaCode(() => EndProgramForce(force: false, isUpdating: isUpdating), 230)
|
||||
}, "Form Close");
|
||||
}
|
||||
else
|
||||
{
|
||||
EndProgramForce(force: false, isUpdating: isUpdating);
|
||||
}
|
||||
|
||||
ModBase.Log("[System] 收到关闭指令");
|
||||
});
|
||||
}
|
||||
|
||||
private static bool isLogShown;
|
||||
|
||||
public static void EndProgramForce(ModBase.ProcessReturnValues returnCode = ModBase.ProcessReturnValues.Success,
|
||||
bool force = true, bool isUpdating = false)
|
||||
{
|
||||
// On Error Resume Next
|
||||
// 关闭联机大厅
|
||||
// Await LobbyController.CloseAsync().ConfigureAwait(False)
|
||||
ModBase.isProgramEnded = true;
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
if (UpdateManager.isUpdateWaitingRestart && !isUpdating)
|
||||
UpdateManager.UpdateRestart(false, false);
|
||||
if (returnCode == ModBase.ProcessReturnValues.Exception)
|
||||
{
|
||||
if (!isLogShown)
|
||||
{
|
||||
ModBase.FeedbackInfo();
|
||||
ModBase.Log("请在 https://github.com/PCL-Community/PCL2-CE/issues 提交错误报告,以便于社区解决此问题!(这也有可能是原版 PCL 的问题)");
|
||||
isLogShown = true;
|
||||
ModBase.ShellOnly(LogWrapper.CurrentLogger.CurrentLogFiles.Last());
|
||||
}
|
||||
|
||||
Thread.Sleep(500); // 防止 PCL 在记事本打开前就被掐掉
|
||||
}
|
||||
|
||||
ModBase.Log("[System] 程序已退出,返回值:" + ModBase.GetStringFromEnum(returnCode));
|
||||
// If ReturnCode <> ProcessReturnValues.Success Then Environment.Exit(ReturnCode)
|
||||
// Process.GetCurrentProcess.Kill()
|
||||
Lifecycle.Shutdown((int)returnCode, force);
|
||||
}
|
||||
|
||||
private void BtnTitleClose_Click(object sender, EventArgs e)
|
||||
{
|
||||
EndProgram(true);
|
||||
}
|
||||
|
||||
// 移动
|
||||
private void FormDragMove(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// On Error Resume Next
|
||||
if (((Grid)sender).IsMouseDirectlyOver)
|
||||
DragMove();
|
||||
}
|
||||
|
||||
// 改变大小
|
||||
/// <summary>
|
||||
/// 是否可以向注册表储存尺寸改变信息。以此避免初始化时误储存。
|
||||
/// </summary>
|
||||
public bool isSizeSaveable;
|
||||
|
||||
private void FormMain_SizeChanged(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
if (isSizeSaveable)
|
||||
{
|
||||
States.UI.WindowHeight = Height;
|
||||
States.UI.WindowWidth = Width;
|
||||
}
|
||||
|
||||
if (PanBack is not null)
|
||||
{
|
||||
RectForm.Rect = new Rect(0d, 0d, PanBack.ActualWidth, PanBack.ActualHeight);
|
||||
|
||||
var formWidth = PanBack.ActualWidth + 0.001d;
|
||||
var formHeight = PanBack.ActualHeight + 0.001d;
|
||||
|
||||
PanForm.Width = formWidth;
|
||||
PanForm.Height = formHeight;
|
||||
PanMain.Width = formWidth;
|
||||
|
||||
if (PanTitle is not null)
|
||||
PanMain.Height = Math.Max(0d, formHeight - PanTitle.ActualHeight);
|
||||
else
|
||||
PanMain.Height = formHeight;
|
||||
|
||||
VideoBack.Width = formWidth;
|
||||
VideoBack.Height = formHeight;
|
||||
}
|
||||
|
||||
if (WindowState == WindowState.Maximized)
|
||||
WindowState = WindowState.Normal; // 修复 #1938
|
||||
}
|
||||
|
||||
// 标题栏改变大小
|
||||
private void PanTitle_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (PanTitleMain.ColumnDefinitions[0].ActualWidth - 30 <= 0)
|
||||
PanTitleLeft.ColumnDefinitions[0].MaxWidth = 0;
|
||||
else
|
||||
PanTitleLeft.ColumnDefinitions[0].MaxWidth = PanTitleMain.ColumnDefinitions[0].ActualWidth - 30;
|
||||
}
|
||||
|
||||
// 最小化
|
||||
private void BtnTitleMin_Click(object sender, EventArgs e)
|
||||
{
|
||||
WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void BtnTitleHelp_Click(object sender, EventArgs e)
|
||||
{
|
||||
ModBase.OpenWebsite("https://www.bilibili.com/video/BV1uT4y1P7CX");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗体事件
|
||||
|
||||
public void AddResizer()
|
||||
{
|
||||
canResize = true;
|
||||
}
|
||||
|
||||
public void RemoveResizer()
|
||||
{
|
||||
canResize = false;
|
||||
}
|
||||
|
||||
// 按键事件
|
||||
private void FormMain_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.IsRepeat)
|
||||
return;
|
||||
// 调用弹窗:回车选择第一个,Esc 选择最后一个
|
||||
if (PanMsg.Children.Count > 0)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
var msg = PanMsg.Children[0];
|
||||
Action? enterAction = msg switch
|
||||
{
|
||||
MyMsgInput input => () => input.Btn1_Click(sender, null),
|
||||
MyMsgSelect select => () => select.Btn1_Click(sender, null),
|
||||
MyMsgText text => () => text.Btn1_Click(sender, null),
|
||||
MyMsgMarkdown markdown => () => markdown.Btn1_Click(sender, null),
|
||||
MyMsgLogin login => () => login.Btn1_Click(sender, null),
|
||||
_ => null
|
||||
};
|
||||
enterAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
var msg = PanMsg.Children[0];
|
||||
Action? escapeAction = msg switch
|
||||
{
|
||||
MyMsgInput input => input.Btn2.Visibility == Visibility.Visible
|
||||
? () => input.Btn2_Click(sender, null)
|
||||
: () => input.Btn1_Click(sender, null),
|
||||
MyMsgSelect select => select.Btn2.Visibility == Visibility.Visible
|
||||
? () => select.Btn2_Click(sender, null)
|
||||
: () => select.Btn1_Click(sender, null),
|
||||
MyMsgText text => text.Btn3.Visibility == Visibility.Visible
|
||||
? () => text.Btn3_Click(sender, null)
|
||||
: text.Btn2.Visibility == Visibility.Visible
|
||||
? () => text.Btn2_Click(sender, null)
|
||||
: () => text.Btn1_Click(sender, null),
|
||||
MyMsgMarkdown markdown => markdown.Btn3.Visibility == Visibility.Visible
|
||||
? () => markdown.Btn3_Click(sender, null)
|
||||
: markdown.Btn2.Visibility == Visibility.Visible
|
||||
? () => markdown.Btn2_Click(sender, null)
|
||||
: () => markdown.Btn1_Click(sender, null),
|
||||
MyMsgLogin login => login.Btn3.Visibility == Visibility.Visible
|
||||
? () => login.Btn3_Click(sender, null)
|
||||
: () => login.Btn1_Click(sender, null),
|
||||
_ => null
|
||||
};
|
||||
escapeAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 按 ESC 返回上一级
|
||||
if (e.Key == Key.Escape)
|
||||
TriggerPageBack();
|
||||
// 更改隐藏实例可见性
|
||||
if (e.Key == Key.F11 && pageCurrent == PageType.InstanceSelect)
|
||||
{
|
||||
ModMain.frmSelectRight.showHidden = !ModMain.frmSelectRight.showHidden;
|
||||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||||
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
|
||||
return;
|
||||
}
|
||||
|
||||
// 更改功能隐藏可见性
|
||||
if (e.Key == Key.F12)
|
||||
{
|
||||
PageSetupUI.HiddenForceShow = !PageSetupUI.HiddenForceShow;
|
||||
if (PageSetupUI.HiddenForceShow)
|
||||
HintService.Hint(Lang.Text("Main.HiddenFeature.Disabled"), HintType.Success);
|
||||
else
|
||||
HintService.Hint(Lang.Text("Main.HiddenFeature.Enabled"), HintType.Success);
|
||||
PageSetupUI.HiddenRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// 按 F5 刷新页面
|
||||
if (e.Key == Key.F5)
|
||||
{
|
||||
if (pageLeft is IRefreshable)
|
||||
((IRefreshable)pageLeft).Refresh();
|
||||
if (pageRight is IRefreshable)
|
||||
((IRefreshable)pageRight).Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用启动游戏
|
||||
if (e.Key == Key.Enter && pageCurrent == PageType.Launch)
|
||||
{
|
||||
if (ModMain.isAprilEnabled && !ModMain.isAprilGiveup)
|
||||
HintService.Hint(Lang.Text("Main.April.Nope"));
|
||||
else
|
||||
ModMain.frmLaunchLeft.LaunchButtonClick();
|
||||
}
|
||||
|
||||
// 修复按下 Alt 后误认为弹出系统菜单导致的冻结
|
||||
if (e.SystemKey == Key.LeftAlt || e.SystemKey == Key.RightAlt)
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void FormMain_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 鼠标侧键返回上一级
|
||||
if (ModMain.frmMain!.PanMsg.Children.Count > 0 || ModMain.WaitingMyMsgBox.Any())
|
||||
return; // 弹窗中(#5513)
|
||||
if (e.ChangedButton == MouseButton.XButton1 || e.ChangedButton == MouseButton.XButton2)
|
||||
TriggerPageBack();
|
||||
}
|
||||
|
||||
private void TriggerPageBack()
|
||||
{
|
||||
if (pageCurrent == PageType.Download && PageCurrentSub == PageSubType.DownloadInstall &&
|
||||
ModMain.frmDownloadInstall.isInSelectPage)
|
||||
ModMain.frmDownloadInstall.ExitSelectPage();
|
||||
else if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionInstall &&
|
||||
ModMain.frmInstanceInstall.isInSelectPage)
|
||||
ModMain.frmInstanceInstall.ExitSelectPage();
|
||||
else
|
||||
PageBack();
|
||||
}
|
||||
|
||||
// 切回窗口
|
||||
private void FormMain_Activated(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Config.Download.Comp.ReadClipboard)
|
||||
ModComp.CompClipboard.GetClipboardResource();
|
||||
if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionMod)
|
||||
{
|
||||
// Mod 管理自动刷新
|
||||
ModMain.frmInstanceMod.ReloadCompFileList();
|
||||
}
|
||||
else if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionResourcePack)
|
||||
{
|
||||
// 资源包管理自动刷新
|
||||
if (ModMain.frmInstanceResourcePack is not null)
|
||||
ModMain.frmInstanceResourcePack.ReloadCompFileList();
|
||||
}
|
||||
else if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionShader)
|
||||
{
|
||||
// 光影包管理自动刷新
|
||||
if (ModMain.frmInstanceShader is not null)
|
||||
ModMain.frmInstanceShader.ReloadCompFileList();
|
||||
}
|
||||
else if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic)
|
||||
{
|
||||
// 投影原理图管理自动刷新
|
||||
if (ModMain.frmInstanceSchematic is not null)
|
||||
ModMain.frmInstanceSchematic.ReloadCompFileList();
|
||||
}
|
||||
else if (pageCurrent == PageType.InstanceSelect)
|
||||
{
|
||||
// 实例选择自动刷新
|
||||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||||
ModLoader.LoaderFolderRunType.RunOnUpdated, 1, @"versions\");
|
||||
}
|
||||
else if (ModMain.frmMain.pageRight is PageInstanceSavesDatapack &&
|
||||
ModMain.frmInstanceSavesDatapack is not null)
|
||||
{
|
||||
// 数据包管理自动刷新
|
||||
ModMain.frmInstanceSavesDatapack.ReloadDatapackFileList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"切回窗口时出错",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private IDataObject _HandleDrag_PrevData;
|
||||
private DragDropEffects _HandleDrag_PrevEffects;
|
||||
|
||||
// 文件拖放
|
||||
private void HandleDrag(object sender, DragEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Handled && e.Effects != DragDropEffects.None)
|
||||
return;
|
||||
// 缓存
|
||||
e.Handled = true;
|
||||
if (ReferenceEquals(e.Data, _HandleDrag_PrevData))
|
||||
{
|
||||
e.Effects = _HandleDrag_PrevEffects;
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定拖放效果
|
||||
e.Effects = DragDropEffects.None;
|
||||
if (e.Data.GetDataPresent(DataFormats.Text))
|
||||
{
|
||||
var str = (string)e.Data.GetData(DataFormats.Text);
|
||||
if (str.StartsWithF("authlib-injector:yggdrasil-server:"))
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
else if (str.StartsWithF("file:///")) e.Effects = DragDropEffects.Copy;
|
||||
}
|
||||
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
|
||||
if (files is not null && files.Length > 0) e.Effects = DragDropEffects.Link;
|
||||
}
|
||||
|
||||
_HandleDrag_PrevData = e.Data;
|
||||
_HandleDrag_PrevEffects = e.Effects;
|
||||
ModBase.Log("[System] 设置拖放类型:" + ModBase.GetStringFromEnum(e.Effects));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"处理拖放时出错",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void FrmMain_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.Text))
|
||||
{
|
||||
// 获取文本
|
||||
try
|
||||
{
|
||||
var str = (string)e.Data.GetData(DataFormats.Text);
|
||||
ModBase.Log("[System] 接受文本拖拽:" + str);
|
||||
if (str.StartsWithF("authlib-injector:yggdrasil-server:"))
|
||||
{
|
||||
// Authlib 拖拽
|
||||
e.Handled = true;
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
var authlibServer =
|
||||
WebUtility.UrlDecode(str.Substring("authlib-injector:yggdrasil-server:".Length));
|
||||
ModBase.Log("[System] Authlib 拖拽:" + authlibServer);
|
||||
if (!new HttpValidator().Validate(authlibServer).IsValid)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.AuthlibInvalid", authlibServer), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModMain.MyMsgBox(Lang.Text("Main.FileDrag.CreateAuthlibProfile", authlibServer), Lang.Text("Main.FileDrag.CreateAuthlibProfileTitle"),
|
||||
Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) == 2)
|
||||
return;
|
||||
ModProfile.selectedProfile = null;
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
PageLoginAuth.draggedAuthServer = authlibServer;
|
||||
ModMain.frmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth);
|
||||
});
|
||||
if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSetup)
|
||||
// 正在服务器选项页,需要刷新设置项显示
|
||||
ModMain.frmInstanceSetup.Reload();
|
||||
}
|
||||
else if (str.StartsWithF("file:///"))
|
||||
{
|
||||
// 文件拖拽(例如从浏览器下载窗口拖入)
|
||||
var filePath = WebUtility.UrlDecode(str).Substring("file:///".Length).Replace("/", @"\");
|
||||
e.Handled = true;
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
FileDrag(new List<string> { filePath });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "无法接取文本拖拽事件", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
// 获取文件并检查
|
||||
var filePathRaw = e.Data.GetData(DataFormats.FileDrop);
|
||||
if (filePathRaw is null) // #2690
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.ExtractFirst"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
e.Effects = DragDropEffects.Link;
|
||||
FileDrag((IEnumerable<string>)filePathRaw);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"接取拖拽事件失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void FileDrag(IEnumerable<string> filePathList)
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
var filePath = filePathList.First();
|
||||
ModBase.Log("[System] 接受文件拖拽:" + filePath + (filePathList.Any() ? $" 等 {filePathList.Count()} 个文件" : ""),
|
||||
ModBase.LogLevel.Developer);
|
||||
// 基础检查
|
||||
if (Directory.Exists(filePathList.First()) && !File.Exists(filePathList.First()))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.FileOnly"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(filePathList.First()))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.FileNotFound", filePathList.First()), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 多文件拖拽
|
||||
if (filePathList.Count() > 1)
|
||||
{
|
||||
// 检查是否为同类型文件
|
||||
var firstExtension = filePathList.First().AfterLast(".").ToLower();
|
||||
var allSameType = filePathList.All(f => (f.AfterLast(".").ToLower() ?? "") == (firstExtension ?? ""));
|
||||
|
||||
if (allSameType &&
|
||||
new[] { "jar", "litemod", "disabled", "old", "litematic", "nbt", "schematic", "schem" }.Contains(
|
||||
firstExtension))
|
||||
{
|
||||
}
|
||||
// 允许同类型的 Mod 文件或投影文件批量拖拽
|
||||
else
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SameTypeOnly"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 主页
|
||||
var extension = filePath.AfterLast(".").ToLower();
|
||||
if (extension == "xaml")
|
||||
{
|
||||
ModBase.Log("[System] 文件后缀为 XAML,作为主页加载");
|
||||
if (File.Exists(ModBase.exePath + @"PCL\Custom.xaml"))
|
||||
if (ModMain.MyMsgBox(Lang.Text("Main.FileDrag.HomepageExists"), Lang.Text("Main.FileDrag.OverwriteTitle"), Lang.Text("Common.Action.Overwrite"), Lang.Text("Common.Action.Cancel")) == 2)
|
||||
return;
|
||||
|
||||
ModBase.CopyFile(filePath, ModBase.exePath + @"PCL\Custom.xaml");
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
Config.Preference.Homepage.Type = 1;
|
||||
ModMain.frmLaunchRight.ForceRefresh();
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.HomepageLoaded"), HintType.Success);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 安装 Mod
|
||||
if (PageInstanceCompResource.InstallMods(filePathList))
|
||||
return;
|
||||
// 安装投影文件
|
||||
if (new[] { "litematic", "nbt", "schematic", "schem" }.Contains(extension))
|
||||
{
|
||||
ModBase.Log($"[System] 文件为 {extension} 格式,尝试作为原理图安装");
|
||||
// 获取当前文件夹路径(如果在资源管理页面)
|
||||
string targetFolderPath = null;
|
||||
if (pageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic &&
|
||||
ModMain.frmInstanceSchematic is not null &&
|
||||
ModMain.frmInstanceSchematic is PageInstanceCompResource)
|
||||
targetFolderPath = ModMain.frmInstanceSchematic.CurrentFolderPath;
|
||||
PageInstanceCompResource.InstallCompFiles(filePathList, ModComp.CompType.Schematic, targetFolderPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理资源安装
|
||||
if (pageCurrent == PageType.InstanceSetup && new[] { "zip" }.Any(i => (i ?? "") == (extension ?? "")))
|
||||
switch (PageCurrentSub)
|
||||
{
|
||||
case PageSubType.VersionWorld:
|
||||
{
|
||||
var destFolder = PageInstanceLeft.McInstance.PathIndie + @"saves\" +
|
||||
ModBase.GetFileNameWithoutExtentionFromPath(filePath);
|
||||
var destLevelDat = Path.Combine(destFolder, "level.dat");
|
||||
if (Directory.Exists(destFolder))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SameFolderExists", destFolder), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var extractFolder = Path.Combine(ModBase.pathTemp, "Cache", "WorldImport", ModBase.GetUuid().ToString());
|
||||
try
|
||||
{
|
||||
ModBase.ExtractFile(filePath, extractFolder);
|
||||
var saveRoot = SaveImportHelper.GetSaveRootDirectory(extractFolder);
|
||||
if (saveRoot is null)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SaveNotFound"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModBase.CopyDirectory(saveRoot, destFolder);
|
||||
if (!File.Exists(destLevelDat))
|
||||
{
|
||||
if (Directory.Exists(destFolder))
|
||||
ModBase.DeleteDirectory(destFolder, true);
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SaveInvalid"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (Directory.Exists(destFolder))
|
||||
ModBase.DeleteDirectory(destFolder, true);
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Main.FileDrag.SaveImportFailed"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Main.FileDrag.SaveImportFailed"));
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(extractFolder))
|
||||
ModBase.DeleteDirectory(extractFolder, true);
|
||||
}
|
||||
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameWithoutExtentionFromPath(filePath)),
|
||||
HintType.Success);
|
||||
if (ModMain.frmInstanceSaves is not null)
|
||||
ModBase.RunInUi(() => ModMain.frmInstanceSaves.Reload());
|
||||
return;
|
||||
}
|
||||
case PageSubType.VersionResourcePack:
|
||||
{
|
||||
var destFile = PageInstanceLeft.McInstance.PathIndie + @"resourcepacks\" +
|
||||
ModBase.GetFileNameFromPath(filePath);
|
||||
if (File.Exists(destFile))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModBase.CopyFile(filePath, destFile);
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success);
|
||||
if (ModMain.frmInstanceResourcePack is not null)
|
||||
ModBase.RunInUi(() => ModMain.frmInstanceResourcePack.ReloadCompFileList());
|
||||
return;
|
||||
}
|
||||
case PageSubType.VersionShader:
|
||||
{
|
||||
var destFile = PageInstanceLeft.McInstance.PathIndie + @"shaderpacks\" +
|
||||
ModBase.GetFileNameFromPath(filePath);
|
||||
if (File.Exists(destFile))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModBase.CopyFile(filePath, destFile);
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success);
|
||||
if (ModMain.frmInstanceShader is not null)
|
||||
ModBase.RunInUi(() => ModMain.frmInstanceShader.ReloadCompFileList());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理投影文件
|
||||
if (pageCurrent == PageType.InstanceSetup &&
|
||||
new[] { "litematic", "nbt", "schematic", "schem" }.Contains(extension) &&
|
||||
PageCurrentSub == PageSubType.VersionSchematic)
|
||||
{
|
||||
var destFile = PageInstanceLeft.McInstance.PathIndie + @"schematics\" +
|
||||
ModBase.GetFileNameFromPath(filePath);
|
||||
if (File.Exists(destFile))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.SameFileExists", destFile), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(PageInstanceLeft.McInstance.PathIndie + @"schematics\");
|
||||
ModBase.CopyFile(filePath, destFile);
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.Imported", ModBase.GetFileNameFromPath(filePath)), HintType.Success);
|
||||
if (ModMain.frmInstanceSchematic is not null)
|
||||
ModBase.RunInUi(() => ModMain.frmInstanceSchematic.ReloadCompFileList());
|
||||
return;
|
||||
}
|
||||
|
||||
// 安装整合包
|
||||
if (new[] { "zip", "rar", "mrpack" }.Any(t =>
|
||||
(t ?? "") == (extension ?? ""))) // 部分压缩包是 zip 格式但后缀为 rar,总之试一试
|
||||
{
|
||||
ModBase.Log("[System] 文件为压缩包,尝试作为整合包安装");
|
||||
try
|
||||
{
|
||||
ModModpack.ModpackInstall(filePath);
|
||||
return;
|
||||
}
|
||||
catch (ModBase.CancelledException ex)
|
||||
{
|
||||
return; // 用户主动取消
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 安装失败,继续往后尝试
|
||||
}
|
||||
}
|
||||
|
||||
// 错误报告分析
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.Log("[System] 尝试进行错误报告分析");
|
||||
var analyzer = new CrashAnalyzer(ModBase.GetUuid());
|
||||
analyzer.Import(filePath);
|
||||
if (!analyzer.Prepare())
|
||||
break;
|
||||
analyzer.Analyze();
|
||||
analyzer.Output(true, new List<string>());
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"自主错误报告分析失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
} while (false);
|
||||
|
||||
// 未知操作
|
||||
HintService.Hint(Lang.Text("Main.FileDrag.UnknownOperation"));
|
||||
}, "文件拖拽");
|
||||
}
|
||||
|
||||
// 接受到 Windows 窗体事件
|
||||
public bool isSystemTimeChanged;
|
||||
|
||||
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
|
||||
{
|
||||
if (msg == 30)
|
||||
{
|
||||
var nowDate = DateTime.Now;
|
||||
if (nowDate.Date == ModBase.applicationOpenTime.Date)
|
||||
{
|
||||
ModBase.Log("[System] 系统时间微调为:" + nowDate.ToLongDateString() + " " + nowDate.ToLongTimeString());
|
||||
isSystemTimeChanged = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[System] 系统时间修改为:" + nowDate.ToLongDateString() + " " + nowDate.ToLongTimeString());
|
||||
isSystemTimeChanged = true;
|
||||
}
|
||||
}
|
||||
else if (msg == 400 * 16 + 2)
|
||||
{
|
||||
ModBase.Log("[System] 收到置顶信息:" + hwnd.ToInt64());
|
||||
if (!isWindowLoadFinished)
|
||||
{
|
||||
ModBase.Log("[System] 窗口尚未加载完成,忽略置顶请求");
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
ShowWindowToTop();
|
||||
handled = true;
|
||||
}
|
||||
else if (msg == 26) // WM_SETTINGCHANGE
|
||||
{
|
||||
if (Marshal.PtrToStringAuto(lParam) == "ImmersiveColorSet")
|
||||
{
|
||||
ModBase.Log($"[System] 系统主题更改,深色模式:{SystemTheme.IsSystemInDarkMode()}");
|
||||
if (Config.Preference.Theme.ColorMode == ColorMode.System &&
|
||||
(ThemeManager.IsDarkMode != SystemTheme.IsSystemInDarkMode())) ThemeService.RefreshColorMode();
|
||||
}
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
// 窗口隐藏与置顶
|
||||
public bool Hidden
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
if (value)
|
||||
{
|
||||
// 隐藏
|
||||
Left -= 10000d;
|
||||
ShowInTaskbar = false;
|
||||
Visibility = Visibility.Hidden;
|
||||
ModBase.Log("[System] 窗口已隐藏,位置:(" + Left + "," + Top + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 取消隐藏
|
||||
if (Left < -2000)
|
||||
Left += 10000d;
|
||||
ShowWindowToTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解决龙猫的非通用实现史山
|
||||
protected override void OnActivated(EventArgs e)
|
||||
{
|
||||
base.OnActivated(e);
|
||||
if (Hidden)
|
||||
Hidden = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把当前窗口拖到最前面。
|
||||
/// </summary>
|
||||
public void ShowWindowToTop()
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
// 这一坨乱七八糟的,别改,改了指不定就炸了,自己电脑还复现不出来
|
||||
Visibility = Visibility.Visible;
|
||||
ShowInTaskbar = true;
|
||||
WindowState = WindowState.Normal;
|
||||
Hidden = false;
|
||||
Topmost = true; // 偶尔 SetForegroundWindow 失效
|
||||
Topmost = false;
|
||||
ModMain.SetForegroundWindow(ModBase.frmHandle);
|
||||
Focus();
|
||||
ModBase.Log($"[System] 窗口已置顶,位置:({Left}, {Top}), {Width} x {Height}");
|
||||
});
|
||||
}
|
||||
|
||||
// 背景视频循环播放
|
||||
private void VideoEnded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
VideoBack.Position = TimeSpan.Zero;
|
||||
VideoBack.Play();
|
||||
}
|
||||
|
||||
// 最小化时暂停背景视频
|
||||
private void WindowStateChanged(object sender, EventArgs e)
|
||||
{
|
||||
switch (WindowState)
|
||||
{
|
||||
case WindowState.Minimized:
|
||||
{
|
||||
ModVideoBack.isMinimized = true;
|
||||
ModVideoBack.VideoPause();
|
||||
break;
|
||||
}
|
||||
case WindowState.Normal:
|
||||
{
|
||||
ModVideoBack.isMinimized = false;
|
||||
ModVideoBack.VideoPlay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 切换页面
|
||||
|
||||
// 页面种类与属性
|
||||
// 注意,这一枚举在 “切换页面” EventType 中调用,应视作公开 API 的一部分
|
||||
/// <summary>
|
||||
/// 页面种类。
|
||||
/// </summary>
|
||||
public enum PageType
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动。
|
||||
/// </summary>
|
||||
Launch = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 下载。
|
||||
/// </summary>
|
||||
Download = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 联机。
|
||||
/// </summary>
|
||||
Tools = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 设置。
|
||||
/// </summary>
|
||||
Setup = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 实例选择。这是一个副页面。
|
||||
/// </summary>
|
||||
InstanceSelect = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 任务管理。这是一个副页面。
|
||||
/// </summary>
|
||||
TaskManager = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 实例设置。这是一个副页面。
|
||||
/// </summary>
|
||||
InstanceSetup = 7,
|
||||
|
||||
/// <summary>
|
||||
/// 资源工程详情。这是一个副页面。
|
||||
/// </summary>
|
||||
CompDetail = 8,
|
||||
|
||||
/// <summary>
|
||||
/// 游戏实时日志。这是一个副页面。
|
||||
/// </summary>
|
||||
GameLog = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 存档详细管理,这是一个副页面。
|
||||
/// </summary>
|
||||
VersionSaves = 12,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 次要页面种类。其数值必须与 StackPanel 中的下标一致。
|
||||
/// </summary>
|
||||
public enum PageSubType
|
||||
{
|
||||
Default = 0,
|
||||
DownloadInstall = 1,
|
||||
DownloadMod = 2,
|
||||
DownloadPack = 3,
|
||||
DownloadDataPack = 4,
|
||||
DownloadResourcePack = 5,
|
||||
DownloadShader = 6,
|
||||
DownloadWorld = 7,
|
||||
DownloadCompFavorites = 8,
|
||||
DownloadClient = 9,
|
||||
DownloadOptiFine = 10,
|
||||
DownloadForge = 11,
|
||||
DownloadNeoForge = 12,
|
||||
DownloadCleanroom = 13,
|
||||
DownloadFabric = 14,
|
||||
DownloadLiteLoader = 16,
|
||||
DownloadLabyMod = 17,
|
||||
DownloadLegacyFabric = 18,
|
||||
|
||||
SetupLaunch = 0,
|
||||
SetupUI = 1,
|
||||
SetupGameManage = 2,
|
||||
SetupLink = 3,
|
||||
SetupAbout = 4,
|
||||
SetupLog = 5,
|
||||
SetupFeedback = 6,
|
||||
SetupGameLink = 7,
|
||||
SetupUpdate = 8,
|
||||
SetupJava = 9,
|
||||
SetupLauncherMisc = 10,
|
||||
SetupLauncherLanguage = 11,
|
||||
|
||||
ToolsGameLink = 1,
|
||||
ToolsTest = 3,
|
||||
|
||||
VersionOverall = 0,
|
||||
VersionSetup = 1,
|
||||
VersionExport = 2,
|
||||
VersionWorld = 3,
|
||||
VersionScreenshot = 4,
|
||||
VersionMod = 5,
|
||||
VersionModDisabled = 6,
|
||||
VersionResourcePack = 7,
|
||||
VersionShader = 8,
|
||||
VersionSchematic = 9,
|
||||
VersionInstall = 10,
|
||||
VersionServer = 11,
|
||||
VersionSavesInfo = 0,
|
||||
VersionSavesDatapack = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
|
||||
/// </summary>
|
||||
private string PageNameGet(PageStackData stack)
|
||||
{
|
||||
switch (stack.page)
|
||||
{
|
||||
case PageType.InstanceSelect:
|
||||
{
|
||||
return Lang.Text("Main.Title.InstanceSelect");
|
||||
}
|
||||
case PageType.TaskManager:
|
||||
{
|
||||
return Lang.Text("Main.Title.TaskManager");
|
||||
}
|
||||
case PageType.GameLog:
|
||||
{
|
||||
return Lang.Text("Main.Title.GameLog");
|
||||
}
|
||||
case PageType.InstanceSetup:
|
||||
{
|
||||
return Lang.Text("Main.Title.InstanceSetup", PageInstanceLeft.McInstance is null ? Lang.Text("Common.State.Unknown") : PageInstanceLeft.McInstance.Name);
|
||||
}
|
||||
case PageType.CompDetail:
|
||||
{
|
||||
return Lang.Text("Main.Title.ResourceDownload", stack.additional.Value.CompProject.TranslatedName);
|
||||
}
|
||||
case PageType.VersionSaves:
|
||||
{
|
||||
return Lang.Text("Main.Title.SaveManagement", ModBase.GetFolderNameFromPath(stack.additional.Value.SavePath));
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新次级页面的名称。
|
||||
/// </summary>
|
||||
public void PageNameRefresh(PageStackData type)
|
||||
{
|
||||
LabTitleInner.Text = PageNameGet(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新次级页面的名称。
|
||||
/// </summary>
|
||||
public void PageNameRefresh()
|
||||
{
|
||||
PageNameRefresh(pageCurrent);
|
||||
}
|
||||
|
||||
// 页面状态存储
|
||||
/// <summary>
|
||||
/// 当前的主页面。
|
||||
/// </summary>
|
||||
public PageStackData pageCurrent = PageType.Launch;
|
||||
|
||||
/// <summary>
|
||||
/// 上一个主页面。
|
||||
/// </summary>
|
||||
public PageStackData pageLast = PageType.Launch;
|
||||
|
||||
/// <summary>
|
||||
/// 当前的子页面。
|
||||
/// </summary>
|
||||
public PageSubType PageCurrentSub
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (pageCurrent.page)
|
||||
{
|
||||
case PageType.Download:
|
||||
{
|
||||
if (ModMain.frmDownloadLeft is null)
|
||||
ModMain.frmDownloadLeft = new PageDownloadLeft();
|
||||
return ModMain.frmDownloadLeft.pageID;
|
||||
}
|
||||
|
||||
case PageType.Setup:
|
||||
{
|
||||
if (ModMain.frmSetupLeft is null)
|
||||
ModMain.frmSetupLeft = new PageSetupLeft();
|
||||
return ModMain.frmSetupLeft.pageID;
|
||||
}
|
||||
|
||||
case PageType.InstanceSetup:
|
||||
{
|
||||
if (ModMain.frmInstanceLeft is null)
|
||||
ModMain.frmInstanceLeft = new PageInstanceLeft();
|
||||
return ModMain.frmInstanceLeft.pageID;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return 0; // 没有子页面
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上层页面的编号堆栈,用于返回。
|
||||
/// </summary>
|
||||
public List<PageStackData> pageStack = new();
|
||||
|
||||
public class PageStackData
|
||||
{
|
||||
/// <summary>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>CompDetail: (CompProject, ExpandedTitles, TargetVersion, TargetLoader, ResourceType)</description></item>
|
||||
/// <item><description>VersionSaves: SavePath</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public (
|
||||
ModComp.CompProject CompProject,
|
||||
List<string> ExpandedTitles,
|
||||
string TargetVersion,
|
||||
ModComp.CompLoaderType TargetLoader,
|
||||
ModComp.CompType ResourceType,
|
||||
string SavePath
|
||||
)? additional;
|
||||
|
||||
public PageType page;
|
||||
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
if (other is PageStackData)
|
||||
{
|
||||
var pageOther = (PageStackData)other;
|
||||
if (page != pageOther.page)
|
||||
return false;
|
||||
if (additional is null) return pageOther.additional is null;
|
||||
|
||||
return pageOther.additional is not null && additional.Equals(pageOther.additional);
|
||||
}
|
||||
|
||||
if (other is int o)
|
||||
{
|
||||
if ((int)page == o)
|
||||
return false;
|
||||
return additional is null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool operator ==(PageStackData left, PageStackData right)
|
||||
{
|
||||
return EqualityComparer<PageStackData>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(PageStackData left, PageStackData right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static implicit operator PageStackData(PageType value)
|
||||
{
|
||||
return new PageStackData { page = value };
|
||||
}
|
||||
|
||||
public static implicit operator PageType(PageStackData value)
|
||||
{
|
||||
return value.page;
|
||||
}
|
||||
}
|
||||
|
||||
public MyPageLeft pageLeft;
|
||||
public MyPageRight pageRight;
|
||||
|
||||
// 引发实际页面切换的入口
|
||||
private bool isChangingPage;
|
||||
|
||||
/// <summary>
|
||||
/// 切换页面,并引起对应选择 UI 的改变。
|
||||
/// </summary>
|
||||
public void PageChange(PageStackData stack, PageSubType subType = PageSubType.Default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PageNameGet(stack)))
|
||||
{
|
||||
// 切换到主页面
|
||||
PageChangeExit();
|
||||
isChangingPage = true; // 防止下面的勾选直接触发了 PageChangeActual
|
||||
((MyRadioButton)PanTitleSelect.Children[(int)stack.page]).SetChecked(true, true,
|
||||
string.IsNullOrEmpty(PageNameGet(pageCurrent)));
|
||||
isChangingPage = false;
|
||||
switch (stack.page)
|
||||
{
|
||||
case PageType.Download:
|
||||
{
|
||||
if (ModMain.frmDownloadLeft is null)
|
||||
ModMain.frmDownloadLeft = new PageDownloadLeft();
|
||||
foreach (var item in ModMain.frmDownloadLeft.PanItem.Children)
|
||||
if (item is MyListItem listItem &&
|
||||
ModBase.Val(listItem.Tag) == (double)subType)
|
||||
{
|
||||
listItem.SetChecked(true, true, stack == pageCurrent);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PageType.Setup:
|
||||
{
|
||||
if (ModMain.frmSetupLeft is null)
|
||||
ModMain.frmSetupLeft = new PageSetupLeft();
|
||||
foreach (var item in ModMain.frmSetupLeft.PanItem.Children)
|
||||
if (item is MyListItem listItem &&
|
||||
ModBase.Val(listItem.Tag) == (double)subType)
|
||||
{
|
||||
listItem.SetChecked(true, true, stack == pageCurrent);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PageChangeActual(stack, subType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 切换到次页面
|
||||
switch (stack.page)
|
||||
{
|
||||
case PageType.InstanceSetup:
|
||||
{
|
||||
if (ModMain.frmInstanceLeft is null)
|
||||
ModMain.frmInstanceLeft = new PageInstanceLeft();
|
||||
foreach (var item in ModMain.frmInstanceLeft.PanItem.Children)
|
||||
if (item is MyListItem listItem &&
|
||||
ModBase.Val(listItem.Tag) == (double)subType)
|
||||
{
|
||||
listItem.SetChecked(true, true, stack == pageCurrent);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PageType.VersionSaves:
|
||||
{
|
||||
if (ModMain.frmInstanceSavesLeft is null)
|
||||
ModMain.frmInstanceSavesLeft = new PageInstanceSavesLeft();
|
||||
foreach (var item in ModMain.frmInstanceSavesLeft.PanItem.Children)
|
||||
if (item is MyListItem listItem &&
|
||||
ModBase.Val(listItem.Tag) == (double)subType)
|
||||
{
|
||||
listItem.SetChecked(true, true, stack == pageCurrent);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PageChangeActual(stack, subType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过点击导航栏改变页面。
|
||||
/// </summary>
|
||||
private void BtnTitleSelect_Click(MyRadioButton sender, bool raiseByMouse)
|
||||
{
|
||||
if (isChangingPage)
|
||||
return;
|
||||
var pageType = (PageType)int.Parse(sender.Tag.ToString());
|
||||
PageChangeActual(pageType, PageSubType.Default);
|
||||
}
|
||||
|
||||
private void BtnTitleInner_Click(object sender, EventArgs e)
|
||||
{
|
||||
PageBack();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过点击返回按钮或手动触发返回来改变页面。
|
||||
/// </summary>
|
||||
public void PageBack()
|
||||
{
|
||||
if (pageStack.Any())
|
||||
PageChangeActual(pageStack[0], PageSubType.Default);
|
||||
else
|
||||
PageChange(PageType.Launch);
|
||||
}
|
||||
|
||||
// 实际处理页面切换
|
||||
/// <summary>
|
||||
/// 切换现有页面的实际方法。
|
||||
/// </summary>
|
||||
private void PageChangeActual(PageStackData stack, PageSubType subType)
|
||||
{
|
||||
if (pageCurrent == stack && (PageCurrentSub == subType || (int)subType == -1))
|
||||
return;
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
try
|
||||
{
|
||||
#region 子页面处理
|
||||
|
||||
var pageName = PageNameGet(stack);
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
{
|
||||
// 即将切换到一个顶级页面
|
||||
PageChangeExit();
|
||||
}
|
||||
// 即将切换到一个子页面
|
||||
else if (pageStack.Any())
|
||||
{
|
||||
// 子页面 → 另一个子页面,更新
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(LabTitleInner, -LabTitleInner.Opacity, 130),
|
||||
ModAnimation.AaCode(() => LabTitleInner.Text = pageName, after: true),
|
||||
ModAnimation.AaOpacity(LabTitleInner, 1d, 150, 30)
|
||||
}, "FrmMain Titlebar SubLayer");
|
||||
if (pageStack.Contains(stack))
|
||||
// 返回到更上层的子页面
|
||||
while (pageStack.Contains(stack))
|
||||
pageStack.RemoveAt(0);
|
||||
else
|
||||
// 进入更深层的子页面
|
||||
pageStack.Insert(0, pageCurrent);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 主页面 → 子页面,进入
|
||||
PanTitleInner.Visibility = Visibility.Visible;
|
||||
PanTitleMain.IsHitTestVisible = false;
|
||||
PanTitleInner.IsHitTestVisible = true;
|
||||
PageNameRefresh(stack);
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PanTitleMain, -PanTitleMain.Opacity, 150),
|
||||
ModAnimation.AaX(PanTitleMain, 12d - PanTitleMain.Margin.Left, 150,
|
||||
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaOpacity(PanTitleInner, 1d - PanTitleInner.Opacity, 150, 200),
|
||||
ModAnimation.AaX(PanTitleInner, -PanTitleInner.Margin.Left, 350, 200,
|
||||
new ModAnimation.AniEaseOutBack()),
|
||||
ModAnimation.AaCode(() => PanTitleMain.Visibility = Visibility.Collapsed, after: true)
|
||||
}, "FrmMain Titlebar FirstLayer");
|
||||
pageStack.Insert(0, pageCurrent);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 实际更改页面框架 UI
|
||||
|
||||
pageLast = pageCurrent;
|
||||
pageCurrent = stack;
|
||||
switch (stack.page)
|
||||
{
|
||||
case PageType.Launch: // 启动
|
||||
{
|
||||
PageChangeAnim(ModMain.frmLaunchLeft, ModMain.frmLaunchRight);
|
||||
break;
|
||||
}
|
||||
case PageType.Download: // 下载
|
||||
{
|
||||
ModMain.frmDownloadLeft ??= new PageDownloadLeft();
|
||||
if (subType != PageSubType.Default)
|
||||
ModMain.frmDownloadLeft.pageID = subType;
|
||||
else
|
||||
subType = ModMain.frmDownloadLeft.pageID;
|
||||
// PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
|
||||
PageChangeAnim(ModMain.frmDownloadLeft, (FrameworkElement)ModMain.frmDownloadLeft.PageGet(subType));
|
||||
break;
|
||||
}
|
||||
case PageType.Tools: // 联机
|
||||
{
|
||||
ModMain.frmToolsLeft ??= new PageToolsLeft();
|
||||
subType = ModMain.frmToolsLeft.pageID;
|
||||
PageChangeAnim(ModMain.frmToolsLeft, (FrameworkElement)ModMain.frmToolsLeft.PageGet(subType));
|
||||
break;
|
||||
}
|
||||
case PageType.Setup: // 设置
|
||||
{
|
||||
ModMain.frmSetupLeft ??= new PageSetupLeft();
|
||||
subType = ModMain.frmSetupLeft.pageID;
|
||||
PageChangeAnim(ModMain.frmSetupLeft, (FrameworkElement)ModMain.frmSetupLeft.PageGet(subType));
|
||||
break;
|
||||
}
|
||||
case PageType.GameLog: // 实时日志
|
||||
{
|
||||
if (ModMain.frmLogLeft is null)
|
||||
ModMain.frmLogLeft = new PageLogLeft();
|
||||
if (ModMain.frmLogLeft is null)
|
||||
ModMain.frmLogRight = new PageLogRight();
|
||||
PageChangeAnim(ModMain.frmLogLeft, ModMain.frmLogRight);
|
||||
break;
|
||||
}
|
||||
case PageType.InstanceSelect: // 实例选择
|
||||
{
|
||||
if (ModMain.frmSelectLeft is null)
|
||||
ModMain.frmSelectLeft = new PageSelectLeft();
|
||||
if (ModMain.frmSelectRight is null)
|
||||
ModMain.frmSelectRight = new PageSelectRight();
|
||||
PageChangeAnim(ModMain.frmSelectLeft, ModMain.frmSelectRight);
|
||||
break;
|
||||
}
|
||||
case PageType.TaskManager: // 任务管理
|
||||
{
|
||||
if (ModMain.frmSpeedLeft is null)
|
||||
ModMain.frmSpeedLeft = new PageSpeedLeft();
|
||||
if (ModMain.frmSpeedRight is null)
|
||||
ModMain.frmSpeedRight = new PageSpeedRight();
|
||||
PageChangeAnim(ModMain.frmSpeedLeft, ModMain.frmSpeedRight);
|
||||
break;
|
||||
}
|
||||
case PageType.InstanceSetup: // 实例设置
|
||||
{
|
||||
ModMain.frmInstanceLeft ??= new PageInstanceLeft();
|
||||
subType = ModMain.frmInstanceLeft.pageID;
|
||||
PageChangeAnim(ModMain.frmInstanceLeft, (FrameworkElement)ModMain.frmInstanceLeft.PageGet(subType));
|
||||
break;
|
||||
}
|
||||
case PageType.CompDetail: // Mod 信息
|
||||
{
|
||||
if (ModMain.frmDownloadCompDetail is null)
|
||||
ModMain.frmDownloadCompDetail = new PageDownloadCompDetail();
|
||||
PageChangeAnim(new MyPageLeft(), ModMain.frmDownloadCompDetail);
|
||||
break;
|
||||
}
|
||||
case PageType.VersionSaves: // 存档管理
|
||||
{
|
||||
if (ModMain.frmInstanceSavesLeft is null)
|
||||
ModMain.frmInstanceSavesLeft = new PageInstanceSavesLeft();
|
||||
PageInstanceSavesLeft.currentSave = stack.additional.Value.SavePath;
|
||||
subType = ModMain.frmInstanceSavesLeft.pageID;
|
||||
PageChangeAnim(ModMain.frmInstanceSavesLeft,
|
||||
(FrameworkElement)ModMain.frmInstanceSavesLeft.PageGet(subType));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 设置为最新状态
|
||||
|
||||
BtnExtraDownload.ShowRefresh();
|
||||
BtnExtraApril.ShowRefresh();
|
||||
|
||||
#endregion
|
||||
|
||||
ModBase.Log("[Control] 切换主要页面:" + ModBase.GetStringFromEnum(stack) + ", " + (int)subType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"切换主要页面失败(ID " + (int)pageCurrent.page + ")",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void PageChangeAnim(FrameworkElement targetLeft, FrameworkElement targetRight)
|
||||
{
|
||||
ModAnimation.AniStop("FrmMain LeftChange");
|
||||
ModAnimation.AniStop("PageLeft PageChange"); // 停止左边栏变更导致的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
// 清除新页面关联性
|
||||
if (targetLeft.Parent is not null)
|
||||
targetLeft.SetValue(ContentPresenter.ContentProperty, null);
|
||||
if (targetRight is not null && targetRight.Parent is not null)
|
||||
targetRight.SetValue(ContentPresenter.ContentProperty, null);
|
||||
pageLeft = (MyPageLeft)targetLeft;
|
||||
pageRight = (MyPageRight)targetRight;
|
||||
// 触发页面通用动画
|
||||
((MyPageLeft)PanMainLeft.Child).TriggerHideAnimation();
|
||||
((MyPageRight)PanMainRight.Child).PageOnExit();
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
// 执行动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
// 把新页面添加进容器
|
||||
PanMainLeft.Child = pageLeft;
|
||||
pageLeft.Opacity = 0d;
|
||||
PanMainLeft.Background = null;
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
ModBase.RunInUi(() => PanMainLeft_Resize(PanMainLeft.ActualWidth), true);
|
||||
}, 110),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
// 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
|
||||
pageLeft.Opacity = 1d;
|
||||
pageLeft.TriggerShowAnimation();
|
||||
}, 30, true)
|
||||
}, "FrmMain PageChangeLeft");
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
((MyPageRight)PanMainRight.Child).PageOnForceExit();
|
||||
// 把新页面添加进容器
|
||||
PanMainRight.Child = pageRight;
|
||||
pageRight.Opacity = 0d;
|
||||
PanMainRight.Background = null;
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
ModBase.RunInUi(() => BtnExtraBack.ShowRefresh(), true);
|
||||
}, 110),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
// 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
|
||||
pageRight.Opacity = 1d;
|
||||
pageRight.PageOnEnter();
|
||||
}, 30, true)
|
||||
}, "FrmMain PageChangeRight");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出子界面。
|
||||
/// </summary>
|
||||
private void PageChangeExit()
|
||||
{
|
||||
if (pageStack.Any())
|
||||
{
|
||||
// 子页面 → 主页面,退出
|
||||
PanTitleMain.Visibility = Visibility.Visible;
|
||||
PanTitleMain.IsHitTestVisible = true;
|
||||
PanTitleInner.IsHitTestVisible = false;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PanTitleInner, -PanTitleInner.Opacity, 150),
|
||||
ModAnimation.AaX(PanTitleInner, -18 - PanTitleInner.Margin.Left, 150,
|
||||
ease: new ModAnimation.AniEaseInFluent()),
|
||||
ModAnimation.AaOpacity(PanTitleMain, 1d - PanTitleMain.Opacity, 150, 200),
|
||||
ModAnimation.AaX(PanTitleMain, -PanTitleMain.Margin.Left, 350, 200,
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => PanTitleInner.Visibility = Visibility.Collapsed, after: true)
|
||||
}, "FrmMain Titlebar FirstLayer");
|
||||
pageStack.Clear();
|
||||
}
|
||||
// 主页面 → 主页面,无事发生
|
||||
}
|
||||
|
||||
// 左边栏改变
|
||||
private void PanMainLeft_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (!e.WidthChanged)
|
||||
return;
|
||||
PanMainLeft_Resize(e.NewSize.Width);
|
||||
}
|
||||
|
||||
private void PanMainLeft_Resize(double newWidth)
|
||||
{
|
||||
var delta = newWidth - RectLeftBackground.Width;
|
||||
if (Math.Abs(delta) > 0.1d && ModAnimation.AniControlEnabled == 0)
|
||||
{
|
||||
if (PanMain.Opacity < 0.1d)
|
||||
PanMainLeft.IsHitTestVisible = false; // 避免左边栏指向背景未能完美覆盖左边栏
|
||||
if (newWidth > 0d)
|
||||
// 宽度足够,显示
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaWidth(RectLeftBackground, newWidth - RectLeftBackground.Width, 180,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
|
||||
ModAnimation.AaOpacity(RectLeftShadow, 1d - RectLeftShadow.Opacity, 180),
|
||||
ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150)
|
||||
}, "FrmMain LeftChange", true);
|
||||
else
|
||||
// 宽度不足,隐藏
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaWidth(RectLeftBackground, -RectLeftBackground.Width, 180,
|
||||
ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 180),
|
||||
ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150)
|
||||
}, "FrmMain LeftChange", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
RectLeftBackground.Width = newWidth;
|
||||
PanMainLeft.IsHitTestVisible = true;
|
||||
ModAnimation.AniStop("FrmMain LeftChange");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 控件拖动
|
||||
|
||||
// 在时钟中调用,使得即使鼠标在窗口外松开,也可以释放控件
|
||||
public void DragTick()
|
||||
{
|
||||
if (ModMain.dragControl is null)
|
||||
return;
|
||||
if (!(Mouse.LeftButton == MouseButtonState.Pressed)) DragStop();
|
||||
}
|
||||
|
||||
// 在鼠标移动时调用,以改变 Slider 位置
|
||||
public void DragDoing()
|
||||
{
|
||||
if (ModMain.dragControl is null)
|
||||
return;
|
||||
if (Mouse.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
ModMain.dragControl.DragDoing();
|
||||
}
|
||||
else
|
||||
DragStop();
|
||||
}
|
||||
|
||||
private void PanBack_MouseMove(object sender, EventArgs e)
|
||||
{
|
||||
DragDoing();
|
||||
}
|
||||
|
||||
public void DragStop()
|
||||
{
|
||||
// 存在其他线程调用的可能性,因此需要确保在 UI 线程运行
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.dragControl is null)
|
||||
return;
|
||||
var control = ModMain.dragControl;
|
||||
ModMain.dragControl = null;
|
||||
control.DragStop(); // 控件会在该事件中判断 DragControl,所以得放在后面
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 附加按钮
|
||||
|
||||
// 更新重启
|
||||
private void BtnExtraUpdateRestart_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
UpdateManager.UpdateRestart(true);
|
||||
}
|
||||
|
||||
private bool BtnExtraUpdateRestart_ShowCheck()
|
||||
{
|
||||
return UpdateManager.isUpdateWaitingRestart;
|
||||
}
|
||||
|
||||
// 音乐
|
||||
private void BtnExtraMusic_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ModMusic.MusicControlPause();
|
||||
}
|
||||
|
||||
private void BtnExtraMusic_RightClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ModMusic.MusicControlNext();
|
||||
}
|
||||
|
||||
// 任务管理
|
||||
private void BtnExtraDownload_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
PageChange(PageType.TaskManager);
|
||||
}
|
||||
|
||||
private bool BtnExtraDownload_ShowCheck()
|
||||
{
|
||||
return ModNet.HasDownloadingTask() && !(pageCurrent == PageType.TaskManager);
|
||||
}
|
||||
|
||||
// 投降
|
||||
public void AprilGiveup()
|
||||
{
|
||||
if (ModMain.isAprilEnabled && !ModMain.isAprilGiveup)
|
||||
{
|
||||
HintService.Hint("=D", HintType.Success);
|
||||
ModMain.isAprilGiveup = true;
|
||||
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleX = 1d;
|
||||
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleY = 1d;
|
||||
BtnExtraApril.ShowRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnExtraApril_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
AprilGiveup();
|
||||
}
|
||||
|
||||
public bool BtnExtraApril_ShowCheck()
|
||||
{
|
||||
return ModMain.isAprilEnabled && !ModMain.isAprilGiveup && pageCurrent == PageType.Launch;
|
||||
}
|
||||
|
||||
// 关闭 Minecraft
|
||||
private void BtnExtraShutdown_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoaderReal is not null)
|
||||
ModLaunch.mcLaunchLoaderReal.Abort();
|
||||
foreach (var Watcher in ModWatcher.mcWatcherList)
|
||||
Watcher.Kill();
|
||||
HintService.Hint(Lang.Text("Main.ShutdownMinecraft.Success"), HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"强制关闭所有 Minecraft 失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Main.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
public bool BtnExtraShutdown_ShowCheck()
|
||||
{
|
||||
return ModWatcher.hasRunningMinecraft;
|
||||
}
|
||||
|
||||
// 游戏日志
|
||||
private void BtnExtraLog_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
PageChange(PageType.GameLog);
|
||||
}
|
||||
|
||||
public bool BtnExtraLog_ShowCheck()
|
||||
{
|
||||
if (ModMain.frmLogLeft is null || ModMain.frmLogRight is null || pageCurrent == PageType.GameLog)
|
||||
return false;
|
||||
return ModMain.frmLogLeft.shownLogs.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回顶部。
|
||||
/// </summary>
|
||||
public void BackToTop()
|
||||
{
|
||||
var realScroll = BtnExtraBack_GetRealChild();
|
||||
if (realScroll is not null)
|
||||
realScroll.PerformVerticalOffsetDelta(-realScroll.VerticalOffset);
|
||||
else
|
||||
ModBase.Log(
|
||||
"[UI] 无法返回顶部,未找到合适的 RealScroll",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Main.Error.ScrollToTopFailed"));
|
||||
}
|
||||
|
||||
private void BtnExtraBack_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
BackToTop();
|
||||
}
|
||||
|
||||
private bool BtnExtraBack_ShowCheck()
|
||||
{
|
||||
var realScroll = BtnExtraBack_GetRealChild();
|
||||
return realScroll is not null && realScroll.Visibility == Visibility.Visible &&
|
||||
realScroll.VerticalOffset > Height + (BtnExtraBack.Show ? 0 : 700);
|
||||
}
|
||||
|
||||
private MyScrollViewer? BtnExtraBack_GetRealChild()
|
||||
{
|
||||
if (PanMainRight.Child is null || !(PanMainRight.Child is MyPageRight))
|
||||
return null;
|
||||
return ((MyPageRight)PanMainRight.Child).PanScroll;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 991 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 791 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 815 B |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 547 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 963 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |