wpf 28

System.IO.IOException

HResult=0x80131620

Message=找不到资源"mainwindow.xaml"。

Source=PresentationFramework

StackTrace:

在 MS.Internal.AppModel.ResourcePart.GetStreamCore(FileMode mode, FileAccess access) 在 MS.Internal.AppModel\ResourcePart.cs 中: 第 40 行

在 System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)

在 MS.Internal.IO.Packaging.PackagePartExtensions.GetSeekableStream(PackagePart packPart, FileMode mode, FileAccess access)

在 System.Windows.Application.LoadComponent(Uri resourceLocator, Boolean bSkipJournaledProperties) 在 System.Windows\Application.cs 中: 第 713 行

在 System.Windows.Application.DoStartup() 在 System.Windows\Application.cs 中: 第 997 行

在 System.Windows.Application.<.ctor>b__1_0(Object unused) 在 System.Windows\Application.cs 中: 第 546 行

在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)

在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

在 System.Windows.Threading.DispatcherOperation.InvokeImpl()

在 MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)

在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

--- 上一位置中堆栈跟踪的末尾 ---

在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

在 MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)

在 System.Windows.Threading.DispatcherOperation.Invoke()

在 System.Windows.Threading.Dispatcher.ProcessQueue()

在 System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)

在 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)

在 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)

在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)

在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

在 System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)

在 MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)

在 MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)

在 System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)

在 System.Windows.Application.RunDispatcher(Object ignore) 在 System.Windows\Application.cs 中: 第 1409 行

在 System.Windows.Application.RunInternal(Window window) 在 System.Windows\Application.cs 中: 第 1084 行

在 WpfAppStudyBoard.App.Main()

csharp 复制代码
using System;
using System.IO;
using System.IO.Packaging;
using System.Resources;
using System.Windows;
using MS.Internal.Resources;

namespace MS.Internal.AppModel;

internal class ResourcePart : PackagePart
{
	private SecurityCriticalDataForSet<ResourceManagerWrapper> _rmWrapper;

	private bool _ensureResourceIsCalled;

	private string _name;

	private readonly object _globalLock = new object();

	public ResourcePart(Package container, Uri uri, string name, ResourceManagerWrapper rmWrapper)
		: base(container, uri)
	{
		if (rmWrapper == null)
		{
			throw new ArgumentNullException("rmWrapper");
		}
		_rmWrapper.Value = rmWrapper;
		_name = name;
	}

	protected override Stream GetStreamCore(FileMode mode, FileAccess access)
	{
		Stream stream = null;
		stream = EnsureResourceLocationSet();
		if (stream == null)
		{
			stream = _rmWrapper.Value.GetStream(_name);
			if (stream == null)
			{
				throw new IOException(SR.Format(SR.UnableToLocateResource, _name));
			}
		}
		ContentType contentType = new ContentType(base.ContentType);
		if (MimeTypeMapper.BamlMime.AreTypeAndSubTypeEqual(contentType))
		{
			stream = new BamlStream(stream, _rmWrapper.Value.Assembly);
		}
		return stream;
	}

	protected override string GetContentTypeCore()
	{
		EnsureResourceLocationSet();
		return MimeTypeMapper.GetMimeTypeFromUri(new Uri(_name, UriKind.RelativeOrAbsolute)).ToString();
	}

	private Stream EnsureResourceLocationSet()
	{
		Stream stream = null;
		lock (_globalLock)
		{
			if (_ensureResourceIsCalled)
			{
				return null;
			}
			_ensureResourceIsCalled = true;
			try
			{
				if (string.Compare(Path.GetExtension(_name), ".baml", StringComparison.OrdinalIgnoreCase) == 0)
				{
					throw new IOException(SR.Format(SR.UnableToLocateResource, _name));
				}
				if (string.Compare(Path.GetExtension(_name), ".xaml", StringComparison.OrdinalIgnoreCase) == 0)
				{
					string name = Path.ChangeExtension(_name, ".baml");
					stream = _rmWrapper.Value.GetStream(name);
					if (stream != null)
					{
						_name = name;
						return stream;
					}
				}
			}
			catch (MissingManifestResourceException)
			{
			}
		}
		return null;
	}
}

说一下textCommand的执行逻辑

csharp 复制代码
[用户在前台输入文本] → "Hello World"
    ↓
[用户点击"发送文本"按钮]
    ↓
[WPF系统执行] → TextCommand.Execute(txtMessage.Text)
    ↓
[Command.Execute调用] → on_execute?.Invoke("Hello World")
    ↓
[执行OnTextSend("Hello World")]
    ↓
[检查参数有效性] → if (parameter is string text) // true
    ↓
[调用硬件方法] → SendOledText("Hello World")
    ↓
[组指令帧] → byte[] cmd = {0x01, 0x10, ... 'H','e','l','l','o'...}
    ↓
[串口发送] → SerialPort.Write(cmd)
    ↓
[更新日志] → LogModel.Add("发送: Hello World")
    ↓
[硬件响应] → 学习卡OLED显示"Hello World"

wpf

自定义控件的本质(打包思维)

自定义控件的核心思想就是:把一堆零散的零件,打包成一个完整的、可重复使用的"成品"。

基本控件(Button, TextBlock)​ = 螺丝、木板、灯泡这些零件。

自定义控件(比如 TemperatureGauge)​ = 用那些零件组装好的、一个带开关和灯罩的完整台灯。

mvvm执行逻辑

前台点击​ -> 触发绑定到 ViewModel 的 Command。

Command 执行​ -> 调用其 Execute方法。

Execute 方法​ -> 调用您关联的具体执行方法(比如 OnSendData)。

执行方法​ -> 调用 Model 层的方法(比如 SerialPortService.Send())来真正操作硬件。

Model 层数据变化(比如收到新数据) -> 通过 INotifyPropertyChanged接口发出通知。

通知发出​ -> View 层因为数据绑定而自动更新界面。

这个理解完全没问题!它准确地描述了 "前台操作如何驱动后台逻辑"​ 的流程。

总结一下最精确的流程:

用户点击 View 上的按钮。

View 触发其绑定的 ICommand(例如 RefreshCommand)。

Command执行其 Execute方法,该方法会调用在 ViewModel 中定义的一个方法(例如 ExecuteRefresh)。

ViewModel 中的这个方法(ExecuteRefresh)调用它所持有的 Model 实例的方法(_deviceModel.ReadDataFromDevice()),由 Model 去执行具体的业务逻辑(如读写串口、操作数据库)。

Model 在执行完逻辑后,可能会更新其内部状态(例如,DeviceModel.LatestData属性发生了变化)。

Model 通过 INotifyPropertyChanged接口发出通知,表示某个属性已更改。(注意:通常是由 Model 或 ViewModel 中包装 Model 数据的属性来发起通知)。

View 层通过数据绑定收到这个通知,然后自动更新 UI。

command

超级简化版总结

ICommand MyCommand { get; }:在 ViewModel 上挖一个固定的、只读的洞(属性),用来放命令。

构造函数 MyCommand = new RelayCommand(...):在创建 ViewModel 时,做一个命令塞进那个洞里。

前台 Command={Binding MyCommand}:按钮找到这个洞里的命令,并绑定它。

所以,整个过程就是:声明一个洞 → 造东西塞进洞 → 前台使用洞里的东西。

就这样!核心就是声明一个只有 get的属性,然后在构造函数里给它第一次、也是唯一一次赋值。

最核心的作用是啥?

就一句话:让按钮的"点击"这个动作,不用在界面后台写代码,而是能直接去执行你 ViewModel 里的一个方法。

没有 ICommand(传统方式):

在按钮的点击事件里写代码。

缺点:界面和逻辑死死绑在一起,没法用 MVVM。

有 ICommand(MVVM 方式):

按钮说:"我不管点击后要干嘛,我的 Command属性指向谁,我就让谁去处理。"

ViewModel 说:"我这里有個 MyCommand,它已经定义好点击后要干嘛了。"

你用 Command={Binding MyCommand}把它们连起来。

最终效果:​ 你在 ViewModel 里写的 保存数据()方法,就能在按钮点击时被调用。而且按钮还能根据 canExecute里的条件自动变灰或变亮!

关于控件(你理解得基本正确,我们稍作精确化)

Button(按钮):​ ✅ 完全正确,就是用来点击的。

Label(标签):​ 它其实是一个文字标签,用来显示一段不可编辑的说明性文字,比如"用户名:"。它通常不是一个"框"。

TextBox(文本框):​ ✅ 完全正确,就是让用户输入文字的框。

Panel(面板):​ ✅ 你的理解"一个区域里面可以弄一些东西"非常到位!它本身是看不见的,主要作用是排列和布局它里面的其他控件(比如按钮、文本框等)。常见的面板有StackPanel(像叠积木一样排列)、Grid(像表格一样划分区域)。

2. WPF 相比 WinForm 的优势(你提到了"更精致、更综合",非常对!)

我们可以把这几点优势具体化:

界面与逻辑彻底分离(这是最大的优势!)

WinForm:​ 界面设计和后台代码(比如按钮点击后做什么)混在一起,改起来麻烦。

WPF:​ 用 XAML​ 语言专门描述界面长什么样(像HTML),用 C#​ 专门写程序逻辑。这样设计师和程序员可以分工合作,效率更高。这就是你感觉它"更综合"的原因。

支持"数据绑定",自动同步

你可以告诉一个文本框:"你的内容就显示UserName这个变量的值"。当UserName变化时,文本框里的字自动就变了,你不需要手动写代码去更新界面。这非常省事!

真正的"硬件加速"和矢量图形

WinForm:​ 界面放大容易模糊,像像素图片。

WPF:​ 界面是矢量图,无论你如何放大缩小窗口,界面都非常清晰,而且因为用了电脑显卡帮忙渲染,做复杂动画也很流畅。这就是你感觉它"更精致"的原因。

强大的样式和模板功能

可以像网站用CSS一样,给所有按钮统一换颜色、字体。你甚至可以用"控件模板"把一个方形的按钮彻底重做成一个圆形的、带光泽的按钮,而它的点击功能不变。

2. 我们来串联一下你描述的完美流程

结合XAML,你描述的那个过程就形成了一个完整的闭环:

① Model(数据模型):

就是一个普通的C#类,比如Student类,里面有Name属性。

② ViewModel(视图模型):

也是一个C#类,它里面包含了一个Student对象。

它实现了INotifyPropertyChanged接口,这样当Student.Name变化时,它能自动发通知。

它实现了ICommand接口,有一个SubmitCommand命令,对应按钮的点击逻辑。

INotifyPropertyChanged:数据的"自动通知系统"

你的理解完全正确!它就是当数据变化时自动发通知的机制。

把它想象成微信的"消息推送":

PropertyChanged事件:就像微信的"消息通道"。

OnPropertyChanged方法:就像点击"发送"按钮。

ICommand:UI操作的"遥控器"

你说ICommand像"触发器",这个比喻很形象!更准确地说,它是一个标准化的遥控器。

为什么需要这个"遥控器"?

在WinForm里,按钮点击要直接写一个事件处理方法,这样界面和逻辑就紧耦合了。

在MVVM里,我们需要一个"中间人",让按钮不知道具体要执行什么,只知道"我按下了,你去执行绑定的命令"。

这个"遥控器"规定了三个最基本的功能:

Execute方法(执行键):"点击后到底要做什么?"​ 比如保存数据、删除文件。

CanExecute方法(状态指示灯):"现在这个按钮能不能按?"​ 比如如果没输入用户名,登录按钮就是灰色的。这个方法返回 true(可用)或 false(不可用)。

CanExecuteChanged事件(状态监听器):当 CanExecute的返回值可能发生变化时,用这个事件通知按钮:"嘿!快重新检查一下你现在该不该变灰!"

所以,ICommand的核心作用就是:在MVVM模式中,把界面操作(如点击)和业务逻辑完美地连接起来,并且能自动管理按钮的可用状态。

一句话总结

INotifyPropertyChanged负责 "数据变,界面跟着变"(自动同步)。

ICommand负责 "点界面,执行后台逻辑"(解耦操作)。

它们俩是 MVVM 模式中连接 View 和 ViewModel 的"桥梁",共同实现了数据和界面的双向自动同步。

③ View(视图界面 - 就是XAML文件):

用XAML"画"出界面:一个TextBox和一个Button。

在XAML里写绑定:

TextBox的Text属性绑定到 {Binding Student.Name}(意思是:这个文本框显示且可以修改Student的Name)

Button的Command属性绑定到 {Binding SubmitCommand}(意思是:这个按钮的点击动作,交给ViewModel的SubmitCommand处理)

④ 魔法般的运行效果:

你在文本框里输入名字(比如"张三"),这个"张三"通过数据绑定自动就写到了后台Student对象的Name属性里。

你点击按钮,这个点击动作通过命令绑定自动触发ViewModel里的SubmitCommand执行。

SubmitCommand里的方法可以去保存或处理这个Student对象。

如果其他地方修改了Student.Name(比如从"张三"改成"李四"),因为ViewModel发了通知,前台的文本框里的字会自动变成"李四"。

Grid布局的正确设置方式(两行两列示例)

不需要嵌套多个Grid,一个Grid就可以实现两行两列。设置方法如下:

xml 复制代码
<Grid>
    <!-- 1. 先定义行和列 -->
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>   <!-- 第一行 -->
        <RowDefinition Height="*"/>   <!-- 第二行 -->
    </Grid.RowDefinitions>
    
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>  <!-- 第一列 -->
        <ColumnDefinition Width="*"/>  <!-- 第二列 -->
    </Grid.ColumnDefinitions>
    
    <!-- 2. 在指定位置放控件 -->
    <TextBlock Grid.Row="0" Grid.Column="0" Text="左上角"/>
    <Button Grid.Row="0" Grid.Column="1" Content="右上角"/>
    <TextBox Grid.Row="1" Grid.Column="0" Text="左下角"/>
    <CheckBox Grid.Row="1" Grid.Column="1" Content="右下角"/>
</Grid>

控件跨列显示

xml 复制代码
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/> <!-- 第一行 -->
        <RowDefinition Height="*"/> <!-- 第二行 -->
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/> <!-- 第一列 -->
        <ColumnDefinition Width="*"/> <!-- 第二列 -->
    </Grid.ColumnDefinitions>
    
    <Button Grid.Row="0" Grid.Column="0" Content="左上"/>
    <Button Grid.Row="0" Grid.Column="1" Content="右上"/>
    <Button Grid.Row="1" Grid.ColumnSpan="2" Content="跨两列"/> <!-- 跨列 -->
</Grid>

1. 样式 vs 控件模板(举例说明)

样式(Style) - "换皮肤"

xml 复制代码
<!-- 定义样式:让所有按钮变成蓝色圆角 -->
<Style x:Key="BlueButtonStyle" TargetType="Button">
    <Setter Property="Background" Value="Blue"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderRadius" Value="10"/>
</Style>

<!-- 使用样式:按钮结构不变,只是外观变了 -->
<Button Style="{StaticResource BlueButtonStyle}" Content="保存"/>

效果:按钮还是那个按钮,只是颜色、圆角变了。就像给人换了件衣服。

控件模板(ControlTemplate) - "重新设计结构"

xml 复制代码
<!-- 定义模板:把按钮彻底重做成圆形带图标的 -->
<ControlTemplate x:Key="CircleButtonTemplate" TargetType="Button">
    <Grid>
        <!-- 完全自定义的视觉结构 -->
        <Ellipse Fill="{TemplateBinding Background}" Stroke="Gray"/>
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Path Data="M12,2L22,12L12,22L2,12Z" Fill="White"/> <!-- 自定义图标 -->
    </Grid>
</ControlTemplate>

<!-- 使用模板:按钮彻底变成了圆形带图标的 -->
<Button Template="{StaticResource CircleButtonTemplate}" Content=""/>

效果:按钮从矩形变成了圆形,还加了图标。就像把一个人重新设计成了机器人

ControlTemplate(控件模板) 则是 "重新定义控件的骨架 + 皮肤"。比如你觉得系统按钮太丑,想自己做一个:左边是图片,中间是文字,右边是小图标,点击时整个区域会缩放 ------ 这时候就需要 ControlTemplate 了。它会完全替换控件的默认结构(相当于把原来的 "骨架" 拆了重搭),你可以自定义控件由哪些元素组成(比如用 StackPanel 拼图片和文字),以及这些元素怎么排列、互动。而且模板定义好后,所有用这个模板的控件都会长得一模一样,实现 "一次定义,到处复用",特别适合做自定义控件时统一外观和结构。

2. 路由事件(Routed Event)

路由事件是WPF特有的事件机制,特点是事件可以在控件树中"传递"。

三种传递方式:

① 冒泡(Bubbling) - 从下往上传递

xml 复制代码
Window(收到事件)
  └── Grid(收到事件)  
    └── StackPanel(收到事件)
      └── Button(事件起源)← 用户点击这里

从Button → StackPanel → Grid → Window 一层层向上传递。

从内向外传递

比喻:你家公寓的门铃响了,整栋楼从下往上都能听到。

xml 复制代码
物业(收到通知)
└── 10楼(收到通知)
  └── 你的公寓(收到通知)  
    └── 你的门铃(源头)← 有人按铃

特点:事件从最内层元素(比如按钮)开始,向父级元素(StackPanel → Grid → Window)一层层传递。

实际应用:在父容器上统一处理子元素的点击。

xml 复制代码
<!-- 在Grid上监听所有内部按钮的点击 -->
<Grid Button.Click="Grid_Click">  
    <Button Content="保存"/>
    <Button Content="取消"/>
</Grid>

这样无论点击哪个按钮,Grid_Click方法都会触发。

② 隧道(Tunneling) - 从上往下传递​

事件名以Preview开头,如PreviewMouseDown

xml 复制代码
Window(先收到Preview事件)
  └── Grid(收到Preview事件)
    └── StackPanel(收到Preview事件)
      └── Button(最后收到Preview事件,然后是普通事件)
  • 从外向内传递

比喻:有人进大楼,保安先知道(Preview),然后才到你家。

特点:事件名以Preview开头(如PreviewMouseDown),从最外层元素向内传递,给机会"预览"或"拦截"事件。

典型用途:在事件到达目标前进行特殊处理(比如按下键盘时先判断是否按了Ctrl键)。

实际例子帮你理解冒泡事件:

xml

xml 复制代码
<StackPanel Background="LightBlue" MouseDown="Panel_MouseDown">
    <Border Background="LightGreen" MouseDown="Border_MouseDown">
        <TextBlock Text="点击我" MouseDown="TextBlock_MouseDown"/>
    </Border>
</StackPanel>
csharp 复制代码
void TextBlock_MouseDown(object sender, MouseButtonEventArgs e) {
    MessageBox.Show("文本块被点击");
    // e.Handled = true; // 如果取消注释,事件就停止传递了
}

void Border_MouseDown(object sender, MouseButtonEventArgs e) {
    MessageBox.Show("边框被点击(事件从文本块冒泡上来了)");
}

void Panel_MouseDown(object sender, MouseButtonEventArgs e) {
    MessageBox.Show("整个面板都被点击了(事件继续冒泡)");
}

当你点击文字时,弹出顺序是:

"文本块被点击" ← 事件起源

"边框被点击" ← 事件冒泡到父级Border

"整个面板都被点击" ← 事件继续冒泡到StackPanel

③ 直接(Direct) - 不传递​

就像WinForm事件,只在触发元素上处理。

实际例子:

xml 复制代码
<StackPanel Button.Click="Panel_Click">  <!-- 在Panel上监听所有子按钮的点击 -->
    <Button Content="按钮1" Click="Button1_Click"/>
    <Button Content="按钮2"/>
</StackPanel>
csharp

// 只有按钮1会触发
void Button1_Click(object sender, RoutedEventArgs e) {
    MessageBox.Show("按钮1被点击");
}

// 所有StackPanel内的按钮点击都会触发这里!
void Panel_Click(object sender, RoutedEventArgs e) {
    var btn = e.OriginalSource as Button; // 获取实际被点击的按钮
    MessageBox.Show($"有人点击了{btn.Content}");
    
    e.Handled = true; // 如果设置为true,事件停止向上传递
}

比喻:你家的私人门铃,只有你自己能听到。

特点:事件只在触发者身上处理,不传递给其他人。

例子:就像WinForm里的事件,按钮点击只在按钮自己身上处理。

路由事件的好处:可以在父容器上统一处理子元素的事件,比如在Grid上监听所有内部按钮的点击

一句话记住它们:

直接事件:自己处理,不传递

冒泡事件:从内向外传递(处理常规交互)

隧道事件:从外向内传递(用于预览和拦截特殊操作)

依赖属性(你的理解完全正确!)

为什么需要依赖属性?

因为只有依赖属性才能支持WPF的这些高级功能:

✅ 数据绑定({Binding ...})

✅ 样式设置()

✅ 动画(属性值动态变化)

✅ 属性值继承(子元素继承父元素的字体、颜色等)

普通CLR属性做不到这些!

OneWay TwoWay 核心区别

OneWay(单向绑定)​

数据流向:仅从数据源(Source)→ 界面控件(Target)​

特点:

数据源变化时,界面自动更新(如后端数据更新,文本框显示新值)

用户修改界面时,数据源不会更新(如修改文本框内容,后端数据不变)

适用场景:只读数据展示(如标签显示用户名、状态提示等)

TwoWay(双向绑定)​

数据流向:数据源 ⇄ 界面控件(双向同步)

特点:

数据源变化 → 界面更新

用户修改界面 → 数据源自动更新(如修改文本框内容,后端数据立即同步)

适用场景:可编辑表单(如输入框、配置选项等)

总结一句话

OneWay= 只读展示(数据变 → 界面变)

TwoWay= 可编辑同步(数据变 ↔ 界面变)

get;和 set;是什么意思?

这是C#属性的访问器。咱们把它想象成一个私密房间的门口保安。

这个房间就是你的数据字段,比如 private string _password;,它很隐私,不能让人随便进。

get和 set就是你看守这个房间的唯一通道。

csharp 复制代码
public string Password
{
    get { return _password; }   // "读"操作。有人想查看密码,保安把值给他。
    set { _password = value; }  // "写"操作。有人想设置新密码,保安把新值存进去。
}

为什么要用"保安"(属性)而不是直接把房间敞开(用公共字段)?

因为有了保安,你就能进行精准控制!

只读属性:只有 get,没有 set。

csharp 复制代码
public string UserId { get; } //  userId只能在构造函数里设一次,其他地方只能读,不能改。

条件设置:在 set里写逻辑。

什么是 "解耦"?怎么理解?

简单说,"解耦" 就是减少代码之间的 "依赖关系",让各个部分能独立工作、单独修改,不用牵一发而动全身。

举个生活例子:

没解耦的情况:比如老式收音机,按钮、喇叭、电路板焊在一起,坏了一个按钮可能整个机器都得拆了修,甚至报废。

解耦的情况:现在的蓝牙音箱,按钮是独立模块,喇叭是独立模块,坏了哪个单独换就行,不影响其他部分。

对应到上位机开发(比如 MVVM 里的解耦):

View(界面)和 ViewModel(逻辑)解耦:改界面按钮位置、颜色,不用动后台登录逻辑;改登录逻辑(比如加验证码),不用动界面代码。

好处:代码更灵活、好维护,多人协作时互不干扰(UI 设计师改界面,开发者写逻辑,不用抢文件)

3. 依赖属性相比普通CLR属性的三大优势是什么?​

先说说 CLR

CLR 全称是 Common Language Runtime(公共语言运行时),它是 .NET 框架的核心,相当于一个 "运行时管家"------ 负责管理代码的执行、内存分配、垃圾回收、安全检查等底层工作。简单说,我们写的 C# 代码最终会被编译成 CLR 能理解的中间语言(IL),然后由 CLR 翻译成机器码执行。你提到的 clr-namespace 是 XAML 里引用 C# 命名空间的语法,比如 xmlns:local="clr-namespace:MyApp",这里的 clr 就是指 CLR 管理的命名空间,用来关联 XAML 和后台代码。

再讲 CLR 属性

CLR 属性就是我们平时在 C# 里用 get/set 定义的属性,比如:

csharp

csharp 复制代码
private string _name;
public string Name {
    get { return _name; }
    set { _name = value; }
}

它本质是对字段的封装,用来控制字段的访问权限(比如只读、只写),还能在获取或设置值时加逻辑(比如验证、触发事件)。这是最基础的属性类型,所有 .NET 语言都通用,由 CLR 直接管理。

本质是 "封装字段",作用是安全地访问和修改类内部的字段(比如加验证逻辑),但它是 "自给自足" 的 ------ 值只存在于当前对象里,不会和其他对象产生关联。

你说的 "构造函数里写的":如果是public class MyClass { public MyClass(string name) { ... } },那构造函数里的name是 "参数",不是属性哦。属性是类对外暴露的 "特征"(比如名字、颜色),而参数是创建对象时传入的 "原材料"。

2. 为什么需要 "依赖属性"(DependencyProperty)?

普通属性满足不了 WPF 的核心需求 ------数据绑定、样式、动画、继承等高级功能。比如你想让按钮的Content绑定到文本框的Text,当文本框内容变了,按钮文字自动更新。这时候普通属性就不够用了:

普通属性的值变化时,没法主动通知绑定的对象 "我变了";

它也不能 "依赖" 其他属性的值(比如按钮的IsEnabled依赖于某个开关状态)。

而依赖属性解决了这些问题:

支持数据绑定:值变化时会自动触发通知(通过PropertyChangedCallback),绑定的 UI 元素会自动更新;

依赖其他值:可以依赖父元素的属性(比如继承父容器的字体)、样式中的设置、动画值等,而不是只能自己存一个固定值;

节省内存:普通属性每个对象都要占一块内存,依赖属性则是多个对象共享一个 "属性定义",只在值和默认值不同时才单独存储,适合大量 UI 元素的场景(比如列表里的 1000 个按钮)。

能联动:

值变了会自动通知绑定的 UI(比如文本框内容变了,按钮文字跟着变),普通属性做不到。

省内存

很多控件共用一个属性时(比如 100 个按钮同个颜色),依赖属性只存一份默认值,普通属性每个都要存,费内存。

更灵活

能被样式、动画、父控件影响(比如改个样式让所有按钮变红色,用动画让按钮慢慢变大),普通属性没这本事。

staticResource和DynamicResource是 WPF 中两种资源引用方式

核心区别在于资源的查找时机和是否支持动态更新,咱用简单的例子说明:

1. StaticResource(静态资源)

查找时机:在XAML 加载时就会去查找资源,一旦找到就 "固定" 下来,之后资源即使变了,引用它的地方也不会更新。

好比你买了一本书,买回来后内容就固定了,哪怕出版社后续修改了版本,你手里的书也不会变。

例子:

xml 复制代码
xml
<Window.Resources>
    <SolidColorBrush x:Key="BtnColor" Color="Blue"/>
</Window.Resources>
csharp 复制代码
<!-- 静态引用 -->
<Button Background="{StaticResource BtnColor}" Content="静态按钮"/>

如果后面通过代码修改了BtnColor的颜色(比如改成红色),按钮的背景不会变,因为静态引用在加载时就已经 "记死" 了最初的蓝色。

2. DynamicResource(动态资源)

查找时机:在程序运行时才会查找资源,并且如果资源后续发生变化,引用它的地方会自动更新。

好比你关注了一个公众号,公众号文章更新后,你打开能看到最新内容,始终同步。

例子:

xml 复制代码
xml
<Window.Resources>
    <SolidColorBrush x:Key="BtnColor" Color="Blue"/>
</Window.Resources>
xml 复制代码
<!-- 动态引用 -->


<Button Background="{DynamicResource BtnColor}" Content="动态按钮"/>

如果通过代码把BtnColor改成红色,按钮的背景会立刻变成红色,因为动态引用会实时 "跟踪" 资源的变化

其实数据绑定(Data Binding)和 DynamicResource 是两种不同的机制,用途不一样哦,别弄混啦~

数据绑定是让控件的属性和 ViewModel 里的数据(比如变量、属性)关联起来,实现 "数据变,UI 跟着变"。比如你在 ViewModel 里有个IsLogin属性,想让按钮的IsEnabled跟着这个属性变,这时候用的是数据绑定,写法是 {Binding IsLogin},和StaticResource/DynamicResource没关系。

而DynamicResource是用来引用资源字典里的资源(比如颜色、样式、模板这些),当资源本身被修改时,引用它的控件会自动更新。比如你定义了一个主题色资源,切换主题时改了这个资源的颜色,用DynamicResource引用的控件会跟着变颜色,这和数据绑定完全是两码事。

grid vs stackPanel

Grid(网格布局)

确实像表格,核心是用 "行(Row)" 和 "列(Column)" 划分区域,每个控件可以指定放在哪一行、哪一列,还能跨行列摆放。比如可以把界面分成 "标题行""内容列""按钮区",每个区域的大小可以固定,也能随窗口拉伸(用*比例分配空间)。适合需要精确排版的场景,比如表单、数据表格,或者界面元素有明确行列关系的时候。

Grid 的 "灵活" 确实体现在行列调整上 ------

可以通过设置 RowDefinition 和 ColumnDefinition 的 Height/Width,用固定值、比例(*)或者自动适应内容(Auto)来分配空间,甚至能让控件跨多行多列,就像搭积木时能自由调整每块积木的位置和大小,特别适合复杂界面的精细排版。

StackPanel(栈式布局)

更像 "排队",要么横向排(Orientation="Horizontal"),要么纵向排(默认 Vertical),控件一个挨着一个放,不会重叠。比如工具栏的按钮横向排列,或者列表项纵向堆叠。它的特点是简单直接,不用手动指定位置,控件会按顺序自动占满空间,但灵活性稍弱 ------ 如果控件太多,可能会超出面板范围,需要配合滚动条使用。

而 StackPanel 的 "固定" 也很明显

它的核心是 "栈" 的逻辑:要么从上到下(纵向)要么从左到右(横向),控件按顺序依次排列,每个控件只占自己需要的空间,后面的控件接着往前一个 "贴" 着放。这种 "死板" 反而成了它的优势 ------ 不用费心设计位置,就能快速排出整齐的线性布局,比如列表、按钮组这些场景,用它既高效又规范。

Margin 和 Padding

都是用来控制空间距离的,但作用对象不一样:

Margin(外边距)

是控件外部的空白区域,控制的是 "这个控件和周围其他控件 / 容器之间的距离"。比如一个按钮的 Margin 设为 10,就意味着它和左边、上边的控件之间会留出 10 单位的空白,避免挤在一起。

Padding(内边距)

是控件内部的空白区域,控制的是 "控件内容和控件边框之间的距离"。比如一个按钮的 Padding 设为 10,那么按钮上的文字(内容)和按钮的边框之间会有 10 单位的距离,让文字不紧贴边框,看起来更舒服。

打个比方:把控件想象成一个相框 ------

Margin 是相框外面和墙壁、其他相框之间的空隙;

Padding 是相框里面,照片和相框边框之间的留白

WPF 里的内存泄露

简单说就是:有些对象明明不用了,但因为被 "意外绑定" 或 "错误引用",导致垃圾回收器(GC)没法回收它们,一直占着内存,越积越多,最后可能让程序变卡甚至崩溃。

常见的场景有这几种,理解了就好避免:

事件没解绑

比如给按钮绑了Click事件,后来按钮被移除了(比如从界面上删掉),但事件没手动解绑,那么按钮对象会一直被事件的 "订阅关系" 牵着,GC 认为它还在用,就不会回收。

数据绑定没断干净

比如 ViewModel 里的对象被 UI 控件通过绑定引用着,后来 ViewModel 不用了,但绑定关系没解除,UI 控件还 "抓着" 它不放,导致 ViewModel 对象一直占内存。

定时器 / 线程没停

如果在控件里开了定时器(比如DispatcherTimer)或者后台线程,控件被销毁后,定时器 / 线程没停止,它们会一直引用着控件对象,导致控件无法被回收。

静态集合 / 变量引用

把某个对象放进静态集合(比如static List),后来不用这个对象了,但没从集合里删掉,静态集合会一直 "记住" 它,导致对象永远不被回收。

简单说,内存泄露的本质就是:"该扔的东西没扔掉,因为还有看不见的'线'牵着它"。写代码时多注意解绑事件、停止定时器、清理静态引用,就能减少这类问题~ 要是怀疑有泄露,可以用 VS 的 "内存诊断工具" 看看哪些对象没被回收,顺藤摸瓜找原因~
相关推荐
baivfhpwxf20232 小时前
WPF Binding 绑定 超详细详解
c#·wpf
数据知道4 小时前
MongoDB心跳检测与故障转移:自动主从切换的全过程解析
数据库·mongodb·wpf
Scout-leaf11 天前
WPF新手村教程(三)—— 路由事件
c#·wpf
柒.梧.14 天前
基于SpringBoot+JWT 实现Token登录认证与登录人信息查询
wpf
十月南城17 天前
Flink实时计算心智模型——流、窗口、水位线、状态与Checkpoint的协作
大数据·flink·wpf
听麟19 天前
HarmonyOS 6.0+ 跨端会议助手APP开发实战:多设备接续与智能纪要全流程落地
分布式·深度学习·华为·区块链·wpf·harmonyos
@hdd19 天前
Kubernetes 可观测性:Prometheus 监控、日志采集与告警
云原生·kubernetes·wpf·prometheus
zls36536520 天前
C# WPF canvas中绘制缺陷分布map
开发语言·c#·wpf
专注VB编程开发20年20 天前
c#Redis扣款锁的设计,多用户,多台电脑操作
wpf