文章目录
- 界面居中
- 配置管理器
- 遇到的问题
-
-
- [Loaded 两次的问题](#Loaded 两次的问题)
-
- 全局捕获异常
- 未响应
-
-
- [1. 耗时操作](#1. 耗时操作)
- [2. 死锁](#2. 死锁)
- [3. 无限循环或长时间的同步等待](#3. 无限循环或长时间的同步等待)
-
- UCEERR_RENDERTHREADFAILURE
-
- [1. 错误含义](#1. 错误含义)
- [2. 堆栈跟踪分析](#2. 堆栈跟踪分析)
- [3. 常见原因](#3. 常见原因)
- 集合已修改;可能无法执行枚举操作
-
- 解决方法
-
- [1. 使用 for 循环](#1. 使用 for 循环)
- [2. 先复制集合,再遍历](#2. 先复制集合,再遍历)
- [3. 将待修改的操作记录下来,在循环结束后统一执行](#3. 将待修改的操作记录下来,在循环结束后统一执行)
- 解决WPF界面卡死等待问题:三种高效处理耗时操作的方法!
本文介绍了在WPF开发中遇到的坑,长期更新
界面居中
xml
WindowStartupLocation = WindowStartupLocation.CenterScreen;
配置管理器
返回的是一个String类型,configuration就是配置的意思,Manager是管理的意思,AppSetting对应配置文件.config里面的标签
在*.cs文件里面定义path,意思就是把在配置文件里面配置好的Path的值赋给path。
csharp
public static string path = ConfigurationManager.AppSettings["Path"];
在*.config文件里面配置以下代码
xml
<appSettings>
<add key="Path" value="D:\Project\FaceControl\Skins\Diamond\DiamondBlue.ssk" />
</appSettings>
代码规范
一般一些涉及到界面修改变化的代码可以不用放到xaml.cs里面,防止xaml.cs内对界面操作代码过冗杂,可以通过binding放到对应viewmodel里
遇到的问题
Loaded 两次的问题
csharp
// 第一时间移除
// 或者UnLoaded
// 可以使用布尔标志进行操作 IsLoaded
public class MyClass : Window
{
public MyClass()
{
Loaded += MyLoadedRoutedEventHandler;
}
void MyLoadedRoutedEventHandler(Object sender, RoutedEventArgs e)
{
Loaded -= MyLoadedRoutedEventHandler;
/// ...
}
};
全局捕获异常
csharp
AppDomain.CurrentDomain.UnhandledException += AppDomainUnhandledException;
Current.DispatcherUnhandledException += CurrentApplication_DispatcherUnhandledException;
Dispatcher.CurrentDispatcher.UnhandledException += CurrentDispatcher_UnhandledException;
AppDomain.CurrentDomain.UnhandledException
AppDomain.CurrentDomain.UnhandledException 事件在 应用程序域 层面捕获所有未被处理的异常,无论这些异常发生在 UI 线程还是后台线程。当一个异常未被任何 try-catch 块捕获,并且传播到其所在线程的顶层时,这个事件就会触发。
- 作用范围: 整个应用程序,包括所有线程(UI 和后台)。
- 线程: 跨线程。它能捕获后台线程(如
Task或Thread)中发生的未处理异常。 - 用途: 这是一个 最后的防线。通常用于记录异常信息并优雅地关闭应用程序,防止程序崩溃。
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 线程上执行一个需要很长时间才能完成的任务时,例如:
- 文件读写:加载或保存大文件。
- 网络请求:同步下载大文件或等待网络 API 响应。
- 复杂计算:进行大量的数学运算、图像处理或数据处理。
- 数据库操作:执行复杂的查询或批量插入/更新。
这些操作会"冻结"UI 线程,导致界面无法更新,按钮无法点击,鼠标光标也无法改变。
2. 死锁
当两个或多个线程互相等待对方释放资源时,就会发生死锁。虽然这通常涉及多个线程,但如果 UI 线程是其中一个被卡住的线程,程序就会进入未响应状态。
3. 无限循环或长时间的同步等待
无限循环:在 UI 线程上执行一个没有退出条件的 while 循环。
同步等待:使用 Task.Wait()、.Result 或 GetAwaiter().GetResult() 来同步等待一个异步任务完成。这会立即阻塞 UI 线程,直到任务完成。
推荐使用以下方案避免出现未响应
-
使用 async/await (推荐)
-
使用 Task.Run
-
使用 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 的机器上,可能会出现图形初始化或渲染上下文丢失的问题。
底层图形操作压力:频繁且复杂的动画、渲染变换或大量几何图形更新,可能会压垮渲染线程。
解决建议
要解决这个错误,您需要从底层图形环境和应用配置两个方面入手:
- 环境与驱动程序检查 (用户侧)
更新/重装显卡驱动: 确保用户运行的是最新的官方显卡驱动程序。
检查 DirectX: 确保系统上的 DirectX 组件是健康且最新的。
系统补丁: 确保操作系统已安装所有最新的更新和补丁。
- 代码和配置调整 (开发者侧)
关闭硬件加速(作为测试):在应用程序启动时,可以通过设置渲染层级来禁用硬件加速,强制使用软件渲染。如果错误消失,说明问题确实出在硬件或驱动上。
// 在 App.xaml.cs 或启动代码中添加
csharp
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+<<Show>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 是你要操作的集合
csharp
for (int i = myList.Count - 1; i >= 0; i--)
{
// 在这里安全地删除元素
if (myList[i].SomeCondition)
{
myList.RemoveAt(i);
}
}
2. 先复制集合,再遍历
你可以先创建一个集合的副本,然后在副本上进行遍历,这样就可以安全地修改原始集合。
// 假设 myList 是你要操作的集合
csharp
var listCopy = myList.ToList();
foreach (var item in listCopy)
{
// 在这里可以安全地修改原始集合 myList
if (item.SomeCondition)
{
myList.Remove(item);
}
}
3. 将待修改的操作记录下来,在循环结束后统一执行
如果你的逻辑比较复杂,需要添加或删除多个元素,可以先将需要修改的元素记录到另一个临时集合中,然后在 foreach 循环结束后,再根据临时集合的内容来操作原始集合。
csharp
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线程
步骤:
- 将耗时操作封装在Task.Run中。
- 使用async/await确保异步执行。
csharp
private async void Button_Click(object sender, RoutedEventArgs e)
{
// UI线程不被阻塞
await Task.Run(() =>
{
// 耗时操作
});
// 更新UI或执行其他UI相关操作
}
2.使用后台线程
优点:
- 简单易实现
- 适用于一些简单的耗时任务
步骤:
- 使用Thread创建后台线程执行耗时操作。
- 利用Dispatcher更新UI。
csharp
private void Button_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() =>
{
// 耗时操作
// 更新UI
this.Dispatcher.Invoke(() =>
{
// 更新UI或执行其他UI相关操作
});
});
// 启动后台线程
thread.Start();
}
3.使用BackgroundWorker
优点:
- 专为UI线程设计
- 提供了进度报告事件
步骤:
- 创建BackgroundWorker实例,处理耗时操作。
- 利用RunWorkerCompleted事件更新UI。
csharp
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可能更为简单和直观。