[奇淫巧技] WPF篇 (长期更新) - 教程

本文介绍了在WPF开发中遇到的坑,长期更新

界面居中

WindowStartupLocation = WindowStartupLocation.CenterScreen;

配置管理器

返回的是一个String类型,configuration就是配置的意思,Manager是管理的意思,AppSetting对应配置文件.config里面的标签

在*.cs文件里面定义path,意思就是把在配置文件里面配置好的Path的值赋给path。

public static string path = ConfigurationManager.AppSettings["Path"];

在*.config文件里面配置以下代码

<appSettings>
  <add key="Path" value="D:\Project\FaceControl\Skins\Diamond\DiamondBlue.ssk" />
</appSettings>

代码规范

一般一些涉及到界面修改变化的代码可以不用放到xaml.cs里面,防止xaml.cs内对界面操作代码过冗杂,可以通过binding放到对应viewmodel里

遇到的问题

Loaded 两次的问题

// 第一时间移除
// 或者UnLoaded
// 可以使用布尔标志进行操作 IsLoaded
public class MyClass : Window
{
public MyClass()
{
Loaded += MyLoadedRoutedEventHandler;
}
void MyLoadedRoutedEventHandler(Object sender, RoutedEventArgs e)
{
Loaded -= MyLoadedRoutedEventHandler;
/// ...
}
};

全局捕获异常

AppDomain.CurrentDomain.UnhandledException += AppDomainUnhandledException;
Current.DispatcherUnhandledException += CurrentApplication_DispatcherUnhandledException;
Dispatcher.CurrentDispatcher.UnhandledException += CurrentDispatcher_UnhandledException;

AppDomain.CurrentDomain.UnhandledException

AppDomain.CurrentDomain.UnhandledException 事件在 应用程序域 层面捕获所有未被处理的异常,无论这些异常发生在 UI 线程还是后台线程。当一个异常未被任何 try-catch 块捕获,并且传播到其所在线程的顶层时,这个事件就会触发。

  • 作用范围: 整个应用程序,包括所有线程(UI 和后台)。
  • 线程: 跨线程。它能捕获后台线程(如 TaskThread)中发生的未处理异常。
  • 用途: 这是一个 最后的防线。通常用于记录异常信息并优雅地关闭应用程序,防止程序崩溃。

Current.DispatcherUnhandledException

Current.DispatcherUnhandledException 事件在 WPF 应用 层面捕获所有未被处理的异常,但仅限于 UI 线程。它是 System.Windows.Application 类的一部分。当 UI 线程中的代码抛出一个未被捕获的异常时,这个事件就会触发。

  • 作用范围: 整个 WPF 应用程序,但仅限于 UI 线程。
  • 线程: UI 线程。它 不会 捕获后台线程的异常。
  • 用途: 主要用于处理 UI 相关的异常。你可以在这里显示一个友好的错误信息给用户,或者记录异常然后决定是否继续运行程序。

Dispatcher.CurrentDispatcher.UnhandledException

Dispatcher.CurrentDispatcher.UnhandledException 事件在 特定线程的调度器 层面捕获未处理的异常。每个线程都有一个 Dispatcher 对象,它负责管理该线程的消息队列和工作项。这个事件只处理在其 关联线程 上发生的未捕获异常。

  • 作用范围: 仅限于其关联的特定线程。
  • 线程: 单个线程,通常是 UI 线程(因为 CurrentDispatcher 多数情况下指的是主 UI 线程的调度器)。
  • 用途: 当你需要为 某个特定的 UI 线程(比如一个独立的、由 Dispatcher 管理的辅助 UI 线程)处理异常时,它会非常有用。

总结与比较

这三个事件共同构成了 WPF 异常处理的层次结构,从特定线程到整个应用程序。

事件作用范围线程触发时机
AppDomain.CurrentDomain.UnhandledException整个应用域所有线程(UI 和后台)最后一个捕获点,程序即将崩溃
Current.DispatcherUnhandledException整个 WPF 应用仅 UI 线程UI 线程发生未捕获异常
Dispatcher.CurrentDispatcher.UnhandledException特定线程仅其关联线程线程调度器发生未捕获异常

在实际开发中,通常会同时订阅这三个事件,以确保在任何情况下都能捕获并处理异常,提高应用的健壮性。

未响应

WPF 程序出现**“未响应”(Not Responding)状态**,通常是由于主 UI 线程被阻塞导致的。WPF 应用程序有一个称为 UI 线程 的单一线程,它负责处理所有用户界面相关的任务,包括绘制界面、响应鼠标点击和键盘输入、处理事件等。

当这个 UI 线程被长时间占用,无法处理 Windows 的消息队列时,操作系统就会判定程序进入“未响应”状态,并在标题栏显示“(未响应)”

1. 耗时操作

这是最常见的原因。当你在 UI 线程上执行一个需要很长时间才能完成的任务时,例如:

  1. 文件读写:加载或保存大文件。
  2. 网络请求:同步下载大文件或等待网络 API 响应。
  3. 复杂计算:进行大量的数学运算、图像处理或数据处理。
  4. 数据库操作:执行复杂的查询或批量插入/更新。

这些操作会“冻结”UI 线程,导致界面无法更新,按钮无法点击,鼠标光标也无法改变。

2. 死锁

当两个或多个线程互相等待对方释放资源时,就会发生死锁。虽然这通常涉及多个线程,但如果 UI 线程是其中一个被卡住的线程,程序就会进入未响应状态。

3. 无限循环或长时间的同步等待

无限循环:在 UI 线程上执行一个没有退出条件的 while 循环。

同步等待:使用 Task.Wait()、.Result 或 GetAwaiter().GetResult() 来同步等待一个异步任务完成。这会立即阻塞 UI 线程,直到任务完成。

推荐使用以下方案避免出现未响应

  1. 使用 async/await (推荐)

  2. 使用 Task.Run

  3. 使用 BackgroundWorker

UCEERR_RENDERTHREADFAILURE

错误分析:UCEERR_RENDERTHREADFAILURE

这个错误的核心是:UCEERR_RENDERTHREADFAILURE,其对应的 HRESULT 码是 0x88980406。

1. 错误含义

UCEERR_RENDERTHREADFAILURE:这是 Unmanaged Code Exception Error - Render Thread Failure 的缩写。它明确指出 WPF 的渲染线程(Render Thread)发生了致命错误。

WPF 架构: WPF 应用程序有两个主要线程:

UI 线程 (或 Dispatcher 线程): 处理用户输入、控件逻辑和数据绑定。

渲染线程: 这是一个独立的、高优先级的线程,负责将视觉树转换为屏幕上的像素。它通过 DUCE (DirectX Unmanaged Code) 与 DirectX/GPU 进行通信。

COMException (HRESULT:0x88980406): 这个异常表明渲染线程在与图形硬件(通过 DirectX 或 DWM/Desktop Window Manager)通信时遇到了问题,导致底层图形系统崩溃。

2. 堆栈跟踪分析

堆栈跟踪清晰地指向了问题的发生位置:

在 System.Windows.Media.Composition.DUCE.Channel.SyncFlush()

在 System.Windows.Interop.HwndTarget.UpdateWindowSettings(…)

DUCE.Channel.SyncFlush(): 这是应用程序的 UI 线程试图将渲染指令(例如更新窗口内容、应用动画等)同步刷新到渲染线程时发生的。

结论: 当 UI 线程尝试与渲染线程同步时,渲染线程已经崩溃或处于无效状态,导致 UI 线程接收到这个致命的异常。

3. 常见原因

这个错误通常不是由您的 C# 应用程序代码直接逻辑错误引起的,而是与运行时环境、图形硬件或驱动程序有关。常见的原因包括:

图形驱动程序问题(最常见):驱动程序陈旧、损坏或与操作系统/WPF 版本不兼容。

硬件加速冲突:应用程序可能试图执行某些图形操作,但 GPU 或 DirectX 环境无法支持。

WPF 宿主环境问题:如果应用程序运行在远程桌面、虚拟化环境或存在多个 GPU 的机器上,可能会出现图形初始化或渲染上下文丢失的问题。

底层图形操作压力:频繁且复杂的动画、渲染变换或大量几何图形更新,可能会压垮渲染线程。

解决建议

要解决这个错误,您需要从底层图形环境和应用配置两个方面入手:

  1. 环境与驱动程序检查 (用户侧)

更新/重装显卡驱动: 确保用户运行的是最新的官方显卡驱动程序。

检查 DirectX: 确保系统上的 DirectX 组件是健康且最新的。

系统补丁: 确保操作系统已安装所有最新的更新和补丁。

  1. 代码和配置调整 (开发者侧)

关闭硬件加速(作为测试):在应用程序启动时,可以通过设置渲染层级来禁用硬件加速,强制使用软件渲染。如果错误消失,说明问题确实出在硬件或驱动上。

// 在 App.xaml.cs 或启动代码中添加

RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;

检查资源清理:确保所有自定义的 DrawingVisual 或复杂的图形资源在使用完毕后都得到了妥善清理,避免资源泄漏导致渲染线程内存不足或混乱。

使用 Dispatcher.Invoke:确保所有对 UI 元素的修改都严格在 UI 线程上进行。虽然此错误通常是渲染线程的问题,但错误的线程操作也可能间接触发图形子系统的崩溃。

[E] 20251013 08:05:56.963 [0001] An unknown exception was received. UCEERR_RENDERTHREADFAILURE (异常来自 HRESULT:0x88980406) {"ClassName":"System.Runtime.InteropServices.COMException","Message":"UCEERR_RENDERTHREADFAILURE (异常来自 HRESULT:0x88980406)","Data":{"System.Object":null},"InnerException":null,"HelpURL":null,"StackTraceString":"   在 System.Windows.Media.Composition.DUCE.Channel.SyncFlush()\r\n   在 System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget, Nullable`1 channelSet)\r\n   在 System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr lParam)\r\n   在 System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)\r\n   在 System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)\r\n   在 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)\r\n   在 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)\r\n   在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)\r\n   在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)","RemoteStackTraceString":null,"RemoteStackIndex":0,"ExceptionMethod":"8\nSyncFlush\nPresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35\nSystem.Windows.Media.Composition.DUCE+Channel\nVoid SyncFlush()","HResult":-2003303418,"Source":"PresentationCore","WatsonBuckets":null}[App.HandleException.0]
[I] 20251013 08:05:56.970 [0001] UserMessageBox Message: An unknown exception was received. UCEERR_RENDERTHREADFAILURE (异常来自 HRESULT:0x88980406)[MessageBoxServiceImpl+<>c__DisplayClass27_0+<b__0>d.MoveNext.0]

核心错误是:UCEERR_RENDERTHREADFAILURE

是 Unmanaged Code Exception Error - Render Thread Failure 的缩写。是WPF的渲染线程发生了致命错误。可能是底层图形操作压力:频繁且复杂的动画、渲染变换或大量几何图形更新,可能会压垮渲染线程。

可以理解成WPF渲染线程的时间跟不上上自动化脚本的速度。

怀疑有两点

渲染指令队列溢出: 自动化测试可能在极短的时间内触发大量的 UI 变化、元素重定位、复杂的布局计算或动画,会导致UI线程向渲染线程发送的指令队列(DUCE.Channel)堆积,

当指令过多,或者渲染线程处理不过来时,可能导致渲染线程因内存或句柄耗尽而崩溃。

内存泄漏/句柄泄漏: 如果应用程序在快速执行测试用例时有小的、未被及时释放的图形资源,快速迭代会迅速将其放大,最终压垮渲染线程。

自动化工具介入操作 UI 元素。这种外部干预可能在 UI 线程和渲染线程之间创造不稳定的同步点。

集合已修改;可能无法执行枚举操作

遇到 “集合已修改;可能无法执行枚举操作” (Collection was modified; enumeration operation may not execute) 这个错误,通常是因为你在使用 foreach 循环遍历一个集合的同时,又在循环内部修改了这个集合。

在 WPF 中,这在处理 UI 绑定时尤为常见。比如,你有一个 ObservableCollection 绑定到 ListBox,然后在遍历这个 ObservableCollection 时又试图添加或删除元素,就会触发这个异常。

为什么会发生这个错误?

foreach 循环在开始遍历时,会创建一个迭代器来跟踪集合的当前状态。如果你在循环体内修改了集合,比如添加或删除了元素,这个状态就会变得不一致,迭代器无法继续安全地执行,因此会抛出异常。

解决方法

1. 使用 for 循环

如果你需要修改集合,可以改用 for 循环,并从集合的末尾向前遍历。这样,即使你删除了元素,索引也不会受到影响。

// 假设 myList 是你要操作的集合

for (int i = myList.Count - 1; i >= 0; i--)
{
// 在这里安全地删除元素
if (myList[i].SomeCondition)
{
myList.RemoveAt(i);
}
}

2. 先复制集合,再遍历

你可以先创建一个集合的副本,然后在副本上进行遍历,这样就可以安全地修改原始集合。

// 假设 myList 是你要操作的集合

var listCopy = myList.ToList();
foreach (var item in listCopy)
{
// 在这里可以安全地修改原始集合 myList
if (item.SomeCondition)
{
myList.Remove(item);
}
}

3. 将待修改的操作记录下来,在循环结束后统一执行

如果你的逻辑比较复杂,需要添加或删除多个元素,可以先将需要修改的元素记录到另一个临时集合中,然后在 foreach 循环结束后,再根据临时集合的内容来操作原始集合。

var itemsToRemove = new List<MyObject>();
  foreach (var item in myList)
  {
  if (item.SomeCondition)
  {
  itemsToRemove.Add(item);
  }
  }
  foreach (var item in itemsToRemove)
  {
  myList.Remove(item);
  }

总结一下,遇到这个错误时,核心思想就是:不要在遍历集合的同时修改它。

解决WPF界面卡死等待问题:三种高效处理耗时操作的方法!

当WPF界面操作中存在耗时的后台处理时,为了避免界面卡死等待问题,可以采用以下解决方法:

1.使用异步操作

优点:

  • 提高应用的响应性
  • 不会阻塞UI线程

步骤:

  1. 将耗时操作封装在Task.Run中。
  2. 使用async/await确保异步执行。
private async void Button_Click(object sender, RoutedEventArgs e)
{
// UI线程不被阻塞
await Task.Run(() =>
{
// 耗时操作
});
// 更新UI或执行其他UI相关操作
}

2.使用后台线程

优点:

  • 简单易实现
  • 适用于一些简单的耗时任务

步骤:

  1. 使用Thread创建后台线程执行耗时操作。
  2. 利用Dispatcher更新UI。
private void Button_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() =>
{
// 耗时操作
// 更新UI
this.Dispatcher.Invoke(() =>
{
// 更新UI或执行其他UI相关操作
});
});
// 启动后台线程
thread.Start();
}

3.使用BackgroundWorker

优点:

  • 专为UI线程设计
  • 提供了进度报告事件

步骤:

  1. 创建BackgroundWorker实例,处理耗时操作。
  2. 利用RunWorkerCompleted事件更新UI。
private BackgroundWorker worker;
private void InitializeBackgroundWorker()
{
worker = new BackgroundWorker();
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// 耗时操作
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// 更新UI或执行其他UI相关操作
}

选择适当的方法取决于项目的需求和复杂性。异步操作通常是最为灵活和强大的解决方案,但在一些情况下,使用后台线程或BackgroundWorker可能更为简单和直观。

posted @ 2026-01-15 20:23  clnchanpin  阅读(2)  评论(0)    收藏  举报