首次提交

This commit is contained in:
GG Z
2026-02-23 10:28:26 +08:00
commit 3461bb9cb3
259 changed files with 22591 additions and 0 deletions

22
WpfAppTest/App.config Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.1.0" newVersion="6.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.4.0" newVersion="4.0.4.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

245
WpfAppTest/App.xaml Normal file
View File

@@ -0,0 +1,245 @@
<Application
x:Class="WpfAppTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:WpfAppTest.Controls"
xmlns:converters="clr-namespace:WpfAppTest.Converter"
xmlns:local="clr-namespace:WpfAppTest"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
StartupUri="ModernWindow.xaml">
<Application.Resources>
<!-- <Color x:Key="RevitBgColor">#F0F0F0</Color>
<Color x:Key="RevitBorderBrush">#D0D0D0</Color>
<Color x:Key="RevitSelectedBgColor">#0078D7</Color>
<Color x:Key="RevitSelectedFgColor">White</Color>
<Color x:Key="RevitHoverBgColor">#ADD8E6</Color>
<Color x:Key="RevitDropdownBgColor">White</Color>
<Color x:Key="RevitDefaultFgColor">Black</Color>
<Color x:Key="RevitDisabledFgColor">#A0A0A0</Color>
<SolidColorBrush x:Key="RevitBgBrush" Color="{StaticResource RevitBgColor}"/>
<SolidColorBrush x:Key="RevitBorderBrush" Color="{StaticResource RevitBorderBrush}"/>
<SolidColorBrush x:Key="RevitSelectedBgBrush" Color="{StaticResource RevitSelectedBgColor}"/>
<SolidColorBrush x:Key="RevitSelectedFgBrush" Color="{StaticResource RevitSelectedFgColor}"/>
<SolidColorBrush x:Key="RevitHoverBgBrush" Color="{StaticResource RevitHoverBgColor}"/>
<SolidColorBrush x:Key="RevitDropdownBgBrush" Color="{StaticResource RevitDropdownBgColor}"/>
<SolidColorBrush x:Key="RevitDefaultFgBrush" Color="{StaticResource RevitDefaultFgColor}"/>
<SolidColorBrush x:Key="RevitDisabledFgBrush" Color="{StaticResource RevitDisabledFgColor}"/>
<DropShadowEffect x:Key="ComboBoxDropShadowEffect"
ShadowDepth="2"
Direction="270"
Color="#000000"
Opacity="0.2"
BlurRadius="5"/>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Padding" Value="6,4,6,4"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource RevitDefaultFgBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource RevitHoverBgBrush}"/>
<Setter Property="Foreground" Value="{StaticResource RevitDefaultFgBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource RevitSelectedBgBrush}"/>
<Setter Property="Foreground" Value="{StaticResource RevitSelectedFgBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource RevitDisabledFgBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}">
<Grid>
<Border x:Name="Border"
Background="{StaticResource RevitBgBrush}" BorderBrush="{StaticResource RevitBorderBrush}" BorderThickness="1" CornerRadius="0"/>
<Path x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,1,0,0"
Fill="{StaticResource RevitDefaultFgBrush}"
Data="M0,0 L4,4 L8,0 Z" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource RevitHoverBgBrush}"/>
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource RevitHoverBgBrush}"/>
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource RevitDefaultFgBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource RevitHoverBgBrush}"/>
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource RevitHoverBgBrush}"/>
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource RevitDefaultFgBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="Background" Value="{StaticResource RevitBgBrush}"/>
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource RevitBorderBrush}"/>
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource RevitDisabledFgBrush}"/>
<Setter TargetName="Border" Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="MyRevitComboBoxStyle" TargetType="{x:Type ComboBox}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Foreground" Value="{StaticResource RevitDefaultFgBrush}"/>
<Setter Property="Background" Value="{StaticResource RevitBgBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource RevitBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="6,2,0,2"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid x:Name="templateRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Bg" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="{StaticResource RevitHoverBgColor}"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Bg" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="{StaticResource RevitHoverBgColor}"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="templateRoot" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<sys:Double>0.6</sys:Double>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Bg" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="{StaticResource RevitHoverBgColor}"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused"/>
<VisualState x:Name="FocusedDropDown"/>
</VisualStateGroup>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid"/>
<VisualState x:Name="InvalidUnfocused"/>
<VisualState x:Name="InvalidFocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="Bg"
Grid.ColumnSpan="2"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="0"/>
<ContentPresenter x:Name="ContentSite"
Grid.Column="0" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
IsHitTestVisible="False"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<ToggleButton x:Name="ToggleButton"
Grid.Column="1" Template="{StaticResource ComboBoxToggleButton}"
Focusable="False"
ClickMode="Press"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBox x:Name="PART_EditableTextBox"
Grid.Column="0" Style="{x:Null}"
Template="{x:Null}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
Visibility="Hidden"
Focusable="True"
IsReadOnly="{TemplateBinding IsReadOnly}"/>
<Popup x:Name="PART_Popup"
AllowsTransparency="True"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
Margin="0"
Grid.ColumnSpan="2">
<Border x:Name="DropDownBorder"
BorderBrush="{StaticResource RevitBorderBrush}"
BorderThickness="1"
Background="{StaticResource RevitDropdownBgBrush}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
MinWidth="{TemplateBinding ActualWidth}"
Effect="{StaticResource ComboBoxDropShadowEffect}">
<ScrollViewer Margin="0" SnapsToDevicePixels="True"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden">
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEditable" Value="True">
<Setter Property="IsTabStop" Value="False"/>
<Setter TargetName="PART_EditableTextBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="ContentSite" Property="Visibility" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>-->
<!--<ResourceDictionary Source="/Generic.xaml" />-->
<ResourceDictionary Source="/ModernStyles.xaml" />
</Application.Resources>
</Application>

17
WpfAppTest/App.xaml.cs Normal file
View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace WpfAppTest
{
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
}
}

237
WpfAppTest/ElementHelper.cs Normal file
View File

@@ -0,0 +1,237 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shell;
namespace ModernWpf.Controls
{
// ==========================================
// 1. 附加属性帮助类 (新增 Prefix)
// ==========================================
public class ElementHelper : DependencyObject
{
// ... 原有的属性保持不变 (Watermark, Icon, CornerRadius, Header) ...
public static readonly DependencyProperty WatermarkProperty = DependencyProperty.RegisterAttached("Watermark", typeof(string), typeof(ElementHelper), new PropertyMetadata(""));
public static void SetWatermark(DependencyObject element, string value) => element.SetValue(WatermarkProperty, value);
public static string GetWatermark(DependencyObject element) => (string)element.GetValue(WatermarkProperty);
public static readonly DependencyProperty IconProperty = DependencyProperty.RegisterAttached("Icon", typeof(Geometry), typeof(ElementHelper), new PropertyMetadata(null));
public static void SetIcon(DependencyObject element, Geometry value) => element.SetValue(IconProperty, value);
public static Geometry GetIcon(DependencyObject element) => (Geometry)element.GetValue(IconProperty);
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.RegisterAttached("CornerRadius", typeof(CornerRadius), typeof(ElementHelper), new PropertyMetadata(new CornerRadius(4)));
public static void SetCornerRadius(DependencyObject element, CornerRadius value) => element.SetValue(CornerRadiusProperty, value);
public static CornerRadius GetCornerRadius(DependencyObject element) => (CornerRadius)element.GetValue(CornerRadiusProperty);
public static readonly DependencyProperty HeaderProperty = DependencyProperty.RegisterAttached("Header", typeof(string), typeof(ElementHelper), new PropertyMetadata(""));
public static void SetHeader(DependencyObject element, string value) => element.SetValue(HeaderProperty, value);
public static string GetHeader(DependencyObject element) => (string)element.GetValue(HeaderProperty);
// === 新增 Prefix 属性 (支持文字或UI元素) ===
public static readonly DependencyProperty PrefixProperty = DependencyProperty.RegisterAttached("Prefix", typeof(object), typeof(ElementHelper), new PropertyMetadata(null));
public static void SetPrefix(DependencyObject element, object value) => element.SetValue(PrefixProperty, value);
public static object GetPrefix(DependencyObject element) => element.GetValue(PrefixProperty);
}
// ==========================================
// 2. ModernWindow (保持不变)
// ==========================================
public class ModernWindow : Window
{
static ModernWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ModernWindow), new FrameworkPropertyMetadata(typeof(ModernWindow)));
}
public ModernWindow()
{
var chrome = new WindowChrome
{
CaptionHeight = 32,
ResizeBorderThickness = new Thickness(4),
// 让系统边框可见,从而触发 Win11 原生圆角和阴影
GlassFrameThickness = new Thickness(1),
UseAeroCaptionButtons = false
};
WindowChrome.SetWindowChrome(this, chrome);
CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (s, e) => SystemCommands.CloseWindow(this)));
CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (s, e) => SystemCommands.MaximizeWindow(this)));
CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (s, e) => SystemCommands.MinimizeWindow(this)));
CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (s, e) => SystemCommands.RestoreWindow(this)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (GetTemplateChild("PART_TitleBar") is Border titleBar)
{
titleBar.MouseLeftButtonDown += (s, e) =>
{
if (e.ClickCount == 2)
{
if (ResizeMode != ResizeMode.NoResize)
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
else
{
DragMove();
}
};
}
}
}
public class SearchableComboBox : ComboBox
{
private TextBox _editableTextBox;
private bool _isInternalOperation;
static SearchableComboBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchableComboBox), new FrameworkPropertyMetadata(typeof(SearchableComboBox)));
}
public SearchableComboBox()
{
IsEditable = true;
IsTextSearchEnabled = false;
StaysOpenOnEdit = true;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_editableTextBox = GetTemplateChild("PART_EditableTextBox") as TextBox;
if (_editableTextBox != null)
{
_editableTextBox.TextChanged += OnEditableTextBoxTextChanged;
// 点击时全选
_editableTextBox.GotFocus += (s, e) => _editableTextBox.SelectAll();
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (_isInternalOperation) return;
_isInternalOperation = true;
base.OnSelectionChanged(e);
// 重点 1选中项改变后必须彻底清空过滤器否则下次打开下拉框只剩下一项
this.Items.Filter = null;
_isInternalOperation = false;
}
private void OnEditableTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
if (_isInternalOperation) return;
string searchText = _editableTextBox.Text;
string selectedText = GetItemDisplayText(SelectedItem);
// 重点 2断开锁定逻辑
// 如果文本框的内容和当前选中项的文本不一致,说明用户正在打字替换内容
if (SelectedItem != null && searchText != selectedText)
{
_isInternalOperation = true;
SelectedItem = null; // 必须将 SelectedItem 设为 null否则 WPF 会强行还原文本
_isInternalOperation = false;
}
// 重点 3执行过滤
this.Items.Filter = item =>
{
if (string.IsNullOrEmpty(searchText)) return true;
string itemText = GetItemDisplayText(item);
// 只要包含关键字就显示(忽略大小写)
return itemText.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
};
// 自动打开下拉框
if (!IsDropDownOpen && _editableTextBox.IsFocused && !string.IsNullOrEmpty(searchText))
{
IsDropDownOpen = true;
}
}
private string GetItemDisplayText(object item)
{
if (item == null) return string.Empty;
if (item is ComboBoxItem cbi) return cbi.Content?.ToString() ?? string.Empty;
// 兼容 DisplayMemberPath 或 TextSearch.TextPath
string path = DisplayMemberPath;
if (string.IsNullOrEmpty(path)) path = TextSearch.GetTextPath(this);
if (!string.IsNullOrEmpty(path))
{
var prop = item.GetType().GetProperty(path);
if (prop != null) return prop.GetValue(item)?.ToString() ?? string.Empty;
}
return item.ToString();
}
}
public class ModernPasswordBox : Control
{
static ModernPasswordBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ModernPasswordBox), new FrameworkPropertyMetadata(typeof(ModernPasswordBox)));
}
public string Password
{
get => (string)GetValue(PasswordProperty);
set => SetValue(PasswordProperty, value);
}
public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register("Password", typeof(string), typeof(ModernPasswordBox), new PropertyMetadata(string.Empty));
// 辅助属性:标记是否为空,用于 XAML 控制水印显隐
public bool IsEmpty
{
get => (bool)GetValue(IsEmptyProperty);
private set => SetValue(IsEmptyProperty, value);
}
public static readonly DependencyProperty IsEmptyProperty = DependencyProperty.Register("IsEmpty", typeof(bool), typeof(ModernPasswordBox), new PropertyMetadata(true));
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var pb = GetTemplateChild("PART_PasswordBox") as PasswordBox;
var tb = GetTemplateChild("PART_TextBox") as TextBox;
var btn = GetTemplateChild("PART_ToggleBtn") as ToggleButton;
if (pb != null && tb != null && btn != null)
{
pb.PasswordChanged += (s, e) =>
{
if (pb.Visibility == Visibility.Visible)
{
Password = pb.Password;
IsEmpty = string.IsNullOrEmpty(pb.Password);
}
};
tb.TextChanged += (s, e) =>
{
if (tb.Visibility == Visibility.Visible)
{
Password = tb.Text;
IsEmpty = string.IsNullOrEmpty(tb.Text);
}
};
btn.Checked += (s, e) => { tb.Text = pb.Password; tb.Visibility = Visibility.Visible; pb.Visibility = Visibility.Collapsed; };
btn.Unchecked += (s, e) => { pb.Password = tb.Text; pb.Visibility = Visibility.Visible; tb.Visibility = Visibility.Collapsed; };
}
}
}
}

Binary file not shown.

BIN
WpfAppTest/Fonts/SJQY.ttf Normal file

Binary file not shown.

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace WpfAppTest
{
public partial class MainViewModel : ObservableValidator
{
[ObservableProperty]
public partial string SampleText { get; set; }
public MainViewModel()
{
SampleText = "TDC(1);800X800;\r\nYJ500x500;\r\nT7C25;B7C25;\r\nG10C20;\r\nA12@100/200(6);\r\n62.24";
}
[ObservableProperty]
[NotifyDataErrorInfo]
[MinLength(1)]
private double pileSpacing;
public object SelectedItem { get; set; }
public List<Student> Students { get; } = new List<Student>
{
new Student("Alice", 20),
new Student("Bob", 22),
new Student("Charlie", 19),
new Student("Diana", 21),
new Student("Ethan", 23)
};
public List<Gender> Genders { get; set; } = new List<Gender> { Gender.Male, Gender.Female };
public List<string> Items { get; } = new List<string>
{
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5"
};
[RelayCommand]
private void Confirm()
{
}
}
public enum Gender
{
Male,
Female,
}
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public int Birth { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int MonthDay { get; set; }
public bool IsSelected { get; set; }
public Gender Gender { get; set; }
public Student() { }
public Student(string name, int age)
{
Name = name;
Age = age;
}
public override string ToString()
{
return $"{Name}, {Age} years old";
}
}
}

1224
WpfAppTest/ModernStyles.xaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
<local:ModernWindow
x:Class="WpfAppTest.ModernWindow"
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:local="clr-namespace:ModernWpf.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:wpfapptest="clr-namespace:WpfAppTest"
Title="ModernWindow"
Width="800"
Height="450"
d:DataContext="{d:DesignInstance Type=wpfapptest:MainViewModel}"
d:Height="800"
Icon="{StaticResource Logo}"
Style="{StaticResource ModernWindowStyle}"
mc:Ignorable="d">
<Window.Resources>
<ResourceDictionary>
<ObjectDataProvider
x:Key="GenderEnumData"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="wpfapptest:Gender" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
</Window.Resources>
<ScrollViewer>
<StackPanel Margin="20">
<!-- 5. TextBox -->
<TextBox Margin="0,0,0,15" local:ElementHelper.Watermark="请输入您的用户名">
<local:ElementHelper.Prefix>
<Path Data="{StaticResource IconUser}" Stroke="{StaticResource LightTextBrush}" />
</local:ElementHelper.Prefix>
</TextBox>
<local:ModernPasswordBox Margin="0,0,0,15" local:ElementHelper.Watermark="请输入密码">
<local:ElementHelper.Prefix>
<Path Data="{StaticResource IconLock}" Stroke="{StaticResource LightTextBrush}" />
</local:ElementHelper.Prefix>
</local:ModernPasswordBox>
<!-- 2. ComboBox (Searchable) -->
<local:SearchableComboBox
Margin="0,0,0,15"
local:ElementHelper.Watermark="搜索..."
IsEditable="True"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}" />
<!-- 8. GroupBox with 6. CheckBox & 7. RadioButton -->
<GroupBox Margin="0,0,0,15" Header="设置选项">
<StackPanel Margin="5">
<CheckBox
Margin="0,5"
Content="记住我"
IsChecked="True" />
<CheckBox Margin="0,5" Content="自动登录" />
<Separator Margin="0,10" />
<RadioButton
Margin="0,5"
Content="普通用户"
GroupName="Role"
IsChecked="True" />
<RadioButton
Margin="0,5"
Content="管理员"
GroupName="Role" />
</StackPanel>
</GroupBox>
<!-- 4. Button with Icon -->
<Button
HorizontalAlignment="Left"
local:ElementHelper.Icon="M12,4L12,20M4,12L20,12"
Command="{Binding ConfirmCommand}"
Content="登 录" />
<DataGrid Margin="5" ItemsSource="{Binding Students}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsSelected}" Header="选择" />
<DataGridTextColumn Binding="{Binding Name}" Header="姓名" />
<DataGridTextColumn Binding="{Binding Age}" Header="年龄" />
<DataGridComboBoxColumn
Header="性别"
ItemsSource="{Binding Genders}"
SelectedItemBinding="{Binding Gender}" />
</DataGrid.Columns>
</DataGrid>
<DataGrid ColumnWidth="*" ItemsSource="{Binding Students}">
<DataGrid.Columns>
<!-- 1. 文本列:【必须】加上 EditingElementStyle -->
<DataGridTextColumn
Width="150"
Binding="{Binding Name}"
EditingElementStyle="{StaticResource ModernEditingTextBoxStyle}"
Header="姓名" />
<DataGridTextColumn
Width="80"
Binding="{Binding Age}"
EditingElementStyle="{StaticResource ModernEditingTextBoxStyle}"
Header="年龄" />
<!-- 2. 复选框列:(TemplateColumn 不需要 EditingElementStyle因为它直接包含 CheckBox) -->
<DataGridTemplateColumn
Width="80"
Header="已选"
SortMemberPath="IsSelected">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- 3. 下拉框列:【必须】加上 EditingElementStyle -->
<DataGridComboBoxColumn
Width="120"
EditingElementStyle="{StaticResource ModernEditingComboBoxStyle}"
Header="性别"
ItemsSource="{Binding Source={StaticResource GenderEnumData}}"
SelectedItemBinding="{Binding Gender}">
<!-- 非编辑状态下的外观优化 -->
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
<wpfapptest:RebarTextBlock RebarText="{Binding SampleText, Mode=TwoWay}" />
<!--<TextBlock FontFamily="{StaticResource SJQY}" Text="CCCC" />-->
</StackPanel>
</ScrollViewer>
</local:ModernWindow>

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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.Media.Imaging;
using System.Windows.Shapes;
namespace WpfAppTest
{
/// <summary>
/// ModernWindow.xaml 的交互逻辑
/// </summary>
public partial class ModernWindow
{
public ModernWindow()
{
DataContext = new MainViewModel();
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace WpfAppTest.Converter
{
public class NullToBooleanConverter : IValueConverter
{
public static readonly NullToBooleanConverter Instance = new NullToBooleanConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value != null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System;
namespace WpfAppTest.Converter
{
public class NullableToVisibilityConverter : IValueConverter
{
/// <summary>
/// The visibility value if the argument is null.
/// </summary>
public Visibility NullValue { get; set; }
public Visibility NotNullValue { get; set; }
/// <summary>
/// Creates a new <see cref="NotNullToVisibilityConverter" />.
/// </summary>
public NullableToVisibilityConverter()
{
NullValue = Visibility.Collapsed;
NotNullValue = Visibility.Visible;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value == null ? NullValue : NotNullValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Windows;
using System.Windows.Controls;
namespace WpfAppTest.Controls // 根据您的项目命名空间修改
{
public static class PasswordBoxHelper
{
public static readonly DependencyProperty HasPasswordProperty =
DependencyProperty.RegisterAttached(
"HasPassword",
typeof(bool),
typeof(PasswordBoxHelper),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static bool GetHasPassword(DependencyObject dp)
{
return (bool)dp.GetValue(HasPasswordProperty);
}
public static void SetHasPassword(DependencyObject dp, bool value)
{
dp.SetValue(HasPasswordProperty, value);
}
public static readonly DependencyProperty MonitorPasswordProperty =
DependencyProperty.RegisterAttached(
"MonitorPassword",
typeof(bool),
typeof(PasswordBoxHelper),
new UIPropertyMetadata(false, OnMonitorPasswordChanged));
public static bool GetMonitorPassword(DependencyObject dp)
{
return (bool)dp.GetValue(MonitorPasswordProperty);
}
public static void SetMonitorPassword(DependencyObject dp, bool value)
{
dp.SetValue(MonitorPasswordProperty, value);
}
private static void OnMonitorPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PasswordBox passwordBox)
{
if ((bool)e.NewValue)
{
passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
UpdateHasPassword(passwordBox); // Initial check
}
else
{
passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
}
}
}
private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
UpdateHasPassword(passwordBox);
}
}
private static void UpdateHasPassword(PasswordBox passwordBox)
{
SetHasPassword(passwordBox, passwordBox.Password.Length > 0);
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("WpfAppTest")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("WpfAppTest")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。 然后取消
//对以下 NeutralResourceLanguage 特性的注释。 更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //主题特定资源词典所处位置
//(未在页面中找到资源时使用,
//或应用程序资源字典中找到时使用)
ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
//(未在页面中找到资源时使用,
//、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本: 4.0.30319.42000
//
// 对此文件的更改可能导致不正确的行为,如果
// 重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace WpfAppTest.Properties
{
/// <summary>
/// 强类型资源类,用于查找本地化字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// 返回此类使用的缓存 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfAppTest.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace WpfAppTest.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfAppTest
{
internal class RebarTextBlock : TextBlock
{
public static readonly DependencyProperty RebarTextProperty =
DependencyProperty.Register(
nameof(RebarText),
typeof(string),
typeof(WpfAppTest.RebarTextBlock),
new PropertyMetadata(string.Empty, OnRebarTextChanged));
public string RebarText
{
get => (string)GetValue(RebarTextProperty);
set => SetValue(RebarTextProperty, value);
}
private static void OnRebarTextChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((WpfAppTest.RebarTextBlock)d).BuildInlines(e.NewValue as string);
}
//{Assembly.GetExecutingAssembly().GetName()}
internal static readonly FontFamily RebarFont = new($"pack://application:,,,/WpfAppTest;component/Fonts/#SJQY");
internal static readonly FontFamily NormalFont = new($"pack://application:,,,/WpfAppTest;component/Fonts/#汉仪粗宋简");
private void BuildInlines(string text)
{
var sjqy = (FontFamily)Application.Current.Resources["SJQY"];
Inlines.Clear();
if (string.IsNullOrEmpty(text)) return;
var regex = new Regex(
@"(?<![A-Za-z])([ABCD])(?=\s*(?:[ØΦ]\d|\d+@\d+))",
RegexOptions.Compiled);
int lastIndex = 0;
foreach (Match match in regex.Matches(text))
{
// 普通文本
if (match.Index > lastIndex)
{
this.Inlines.Add(new Run
{
Text = text.Substring(lastIndex, match.Index - lastIndex),
FontFamily = NormalFont
});
}
// 钢筋等级SJQY
this.Inlines.Add(new Run
{
Text = match.Value,
FontFamily = sjqy
});
lastIndex = match.Index + match.Length;
}
// 尾部普通文本
if (lastIndex < text.Length)
{
this.Inlines.Add(new Run
{
Text = text.Substring(lastIndex),
});
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace WpfAppTest
{
internal class UnitConverter : IValueConverter
{
private string DefaultUnit { get; set; } = "mm";
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
if (value is double d)
{
string unit = parameter as string ?? DefaultUnit;
// 使用StringFormat时value会先被格式化这里直接拼接单位
return $"{d.ToString(culture)} {unit}";
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string str)
{
// 提取纯数字字符串(允许中间态如 '1.', '-.'
str = str.Trim();
var match = Regex.Match(str, @"[-+]?[0-9]*\.?[0-9]*");
if (match.Success)
{
string numberPart = match.Value;
// 处理中间态输入,允许返回未完成的文本(让 UI 不抖动)
if (string.IsNullOrEmpty(numberPart) || numberPart.EndsWith("-") || numberPart.EndsWith(".") || numberPart.EndsWith("-."))
return DependencyProperty.UnsetValue;
if (double.TryParse(numberPart, NumberStyles.Any, culture, out double result))
return result;
}
}
return DependencyProperty.UnsetValue;
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
namespace WpfAppTest.Behavior
{
public static class WindowBehavior
{
// 这是我们的附加属性,用于启用自定义窗口行为
public static readonly DependencyProperty EnableCustomWindowBehaviorProperty =
DependencyProperty.RegisterAttached(
"EnableCustomWindowBehavior",
typeof(bool),
typeof(WindowBehavior),
new PropertyMetadata(false, OnEnableCustomWindowBehaviorChanged));
public static bool GetEnableCustomWindowBehavior(DependencyObject obj)
{
return (bool)obj.GetValue(EnableCustomWindowBehaviorProperty);
}
public static void SetEnableCustomWindowBehavior(DependencyObject obj, bool value)
{
obj.SetValue(EnableCustomWindowBehaviorProperty, value);
}
// 当 EnableCustomWindowBehavior 属性值改变时触发
private static void OnEnableCustomWindowBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Window window)
{
if ((bool)e.NewValue)
{
// 当启用行为时,在窗口加载后查找并绑定元素
window.Loaded += Window_Loaded;
}
else
{
// 当禁用行为时,解除绑定(如果需要更彻底的清理,可以做更多操作)
window.Loaded -= Window_Loaded;
}
}
}
// 窗口加载后执行,此时 ControlTemplate 已经被应用
private static void Window_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Window window)
{
// *** 关键:通过 FindName 在 ControlTemplate 中查找元素 ***
// 这些元素的 x:Name 必须与你的 Window 样式模板中的名称一致!
Button minimizeButton = window.Template.FindName("MinimizeButton", window) as Button;
Button maximizeRestoreButton = window.Template.FindName("MaximizeRestoreButton", window) as Button;
Button closeButton = window.Template.FindName("CloseButton", window) as Button;
Border titleBarDragArea = window.Template.FindName("WindowBorder", window) as Border; // 或者你的标题栏 Border 的 x:Name
Border resizeGrip = window.Template.FindName("PART_ResizeGrip", window) as Border; // 调整大小手柄
// --- 绑定按钮点击事件 ---
if (minimizeButton != null)
{
minimizeButton.Click += (s, ev) => window.WindowState = WindowState.Minimized;
}
if (maximizeRestoreButton != null)
{
maximizeRestoreButton.Click += (s, ev) =>
{
if (window.WindowState == WindowState.Normal)
window.WindowState = WindowState.Maximized;
else
window.WindowState = WindowState.Normal;
};
}
if (closeButton != null)
{
closeButton.Click += (s, ev) => window.Close();
}
// --- 绑定标题栏拖拽和双击最大化事件 ---
if (titleBarDragArea != null)
{
// 单击并拖动以移动窗口
titleBarDragArea.MouseLeftButtonDown += (s, ev) =>
{
if (ev.ClickCount == 1 && window.WindowState == WindowState.Normal)
{
window.DragMove(); // WPF 内置的拖动方法
}
};
// 双击标题栏最大化/还原窗口
titleBarDragArea.MouseLeftButtonDown += (s, ev) =>
{
if (ev.ClickCount == 2)
{
if (window.WindowState == WindowState.Normal)
window.WindowState = WindowState.Maximized;
else
window.WindowState = WindowState.Normal;
}
};
}
// --- 处理调整大小手柄 ---
if (resizeGrip != null)
{
resizeGrip.PreviewMouseLeftButtonDown += (s, ev) =>
{
if (window.WindowState == WindowState.Normal)
{
// 调用 Win32 API 来启动窗口调整大小
IntPtr handle = new WindowInteropHelper(window).Handle;
SendMessage(handle, 0x0112, (IntPtr)0xF008, IntPtr.Zero); // WM_SYSCOMMAND, SC_SIZE (bottom-right)
ev.Handled = true; // 阻止事件冒泡,避免与窗口拖动冲突
}
};
}
}
}
// --- P/Invoke: 调用 Windows API 的 SendMessage 函数 ---
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<LangVersion>preview</LangVersion>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data.DataSetExtensions" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="System.Buffers" Version="4.6.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Memory" Version="4.6.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.6.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>
<ItemGroup>
<Resource Include="Fonts\HanYiCuSongJian.ttf" />
<Resource Include="Fonts\SJQY.ttf" />
</ItemGroup>
</Project>