窗体快速取消

================================================================================

「后台任务随时取消」完整技术方案(原理 + 函数 + 新手步骤 + 完整代码)

适用场景:AutoCAD C# 二次开发 / .NET 桌面应用 耗时计算取消

================================================================================

版本:v2.0(二合一完整版)

目标读者:从零基础新手到需要深入原理的开发者

阅读时间:完整阅读约 30 分钟,按步骤操作约 15 分钟

═══════════════════════════════════════════════════════════════════════════════

第一部分:核心原理(为什么要这样做)

═══════════════════════════════════════════════════════════════════════════════

1.1 什么是"协作式取消"?

─────────────────────────────────────────────────────────────────────────────

不是强制终止线程(如 Thread.Abort),而是通过一个共享的布尔变量作为"停止旗帜",

让所有正在运行的代码主动"看见"并"退让"。

流程:

用户点击取消

设置取消标志 = true

耗时循环每次迭代都检查该标志

发现标志为 true → 立即 break/return

清理资源 → 退出

优点:安全(不会破坏非托管资源)、可控(在代码安全点退出)、兼容性好。

1.2 为什么不能直接用 Thread.Abort()?

─────────────────────────────────────────────────────────────────────────────

Thread.Abort() 会抛出 ThreadAbortException,强制中断任何代码。

如果中断发生在 using 块、事务(Transaction)或原生 DLL 调用中,

会导致资源泄漏、CAD 崩溃或数据库损坏。

在 AutoCAD 二次开发中,禁止使用 Thread.Abort()。

1.3 为什么需要后台线程 + 定时器?

─────────────────────────────────────────────────────────────────────────────

如果不使用后台线程,耗时计算会阻塞 UI 线程,取消按钮无法响应点击事件。

如果仅使用后台线程但不用定时器,后台线程无法安全地更新 UI 控件(进度条、文字)。

定时器在 UI 线程上触发 Tick 事件,可以安全地检查取消标志并更新 UI。

1.4 为什么需要消息队列(ConcurrentQueue)?

─────────────────────────────────────────────────────────────────────────────

后台线程不能直接调用 ed.WriteMessage()(AutoCAD Editor 不是线程安全的)。

使用 ConcurrentQueue 暂存日志消息,由 UI 定时器在 UI 线程中消费并输出。

ConcurrentQueue 是线程安全的,无需额外加锁。

1.5 volatile 关键字的作用?

─────────────────────────────────────────────────────────────────────────────

volatile 告诉编译器和 CPU:该变量可能被多个线程修改,不要对它做优化缓存。

保证一个线程对变量的修改,能被其他线程立即看到(内存屏障)。

如果不加 volatile,在 Release 编译模式下,循环可能因为 CPU 缓存而永远看不到标志变化。

1.6 新旧项目对比:为什么旧项目秒停,新项目卡住?

─────────────────────────────────────────────────────────────────────────────

旧项目(CadNest V4.7.5):

  • 求解器有独立的 Cancel() 方法(可能通过进程间通信终止外部进程)

  • 计算任务被拆碎成细粒度并行(Parallel.ForEach),每处理完一个零件就检查取消标志

  • 取消响应时间 = 当前零件计算耗时(通常毫秒级)

新项目(DeepNest):

  • 深层循环(Background.placeParts)没有取消检查点

  • 原生 DLL(minkowski.dll)无法从 C# 层中断

  • 取消响应时间 = 当前原生函数调用耗时(可能数秒)

解决方案:

  • 在所有托管循环中添加取消检查

  • 原生 DLL 无法中断,只能等待返回,但至少在循环间隙能立即响应

═══════════════════════════════════════════════════════════════════════════════

第二部分:用到的关键技术点与函数详解

═══════════════════════════════════════════════════════════════════════════════

2.1 命名空间引用(所有需要添加的 using)

─────────────────────────────────────────────────────────────────────────────

using System;

using System.Threading;

using System.Threading.Tasks; // Task.Run

using System.Collections.Concurrent; // ConcurrentQueue

using System.Windows.Forms; // Timer, Application.DoEvents

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

2.2 核心函数/属性清单

─────────────────────────────────────────────────────────────────────────────

【取消标志】

public static volatile bool CancelRequested;

作用:全局共享的取消标志,所有线程可见。

【进度窗体】

public bool IsCancelled { get; private set; }

作用:用户是否点击了取消按钮。

btnCancel.Click += (s, e) => { this.IsCancelled = true; }

作用:点击取消按钮时设置标志。

【命令入口】

CommandMethod("命令名") public void 命令名()

作用:AutoCAD 命令入口,重置标志并启动异步流程。

【异步编排】

private void RunDirectNestingAsync(...)

作用:启动后台任务和定时器,协调进度更新和收尾。

Task.Run(() => { ... })

作用:在后台线程池中执行耗时计算。

System.Windows.Forms.Timer _pollTimer;

作用:UI 定时器,每 200ms 检查取消状态和任务完成状态。

_pollTimer.Tick += (s, e) => { ... }

作用:定时器触发时的回调函数。

【消息队列】

private static ConcurrentQueue<string> _messageQueue

作用:线程安全的日志消息队列。

_messageQueue.Enqueue("消息");

作用:后台线程入队日志。

_messageQueue.TryDequeue(out string msg)

作用:定时器出队并输出日志。

【跨线程 UI 更新】

progress.Invoke((MethodInvoker)delegate { ... });

作用:在 UI 线程上执行委托,安全更新进度条。

try { ... } catch (ObjectDisposedException) { break; }

作用:捕获窗体已关闭的异常,避免程序崩溃。

【资源清理】

progress.Close();

作用:关闭进度窗体,释放资源。

_pollTimer.Stop(); _pollTimer = null;

作用:停止定时器,允许垃圾回收。

【AutoCAD 文档锁定】

using (DocumentLock docLock = doc.LockDocument())

作用:在操作数据库前锁定文档,防止冲突。

using (Transaction tr = db.TransactionManager.StartTransaction())

作用:事务管理,确保数据库操作原子性。

═══════════════════════════════════════════════════════════════════════════════

第三部分:分步实施指南(新手操作步骤)

═══════════════════════════════════════════════════════════════════════════════

【前置准备】

  1. 确认你的项目是 .NET Framework 4.8 或更高版本(支持 Task.Run)。

  2. 确认项目引用了 System.Windows.Forms(用于 Timer 和 ProgressForm)。

  3. 确认项目引用了 AutoCAD .NET API 库(acdbmgd.dll、acmgd.dll)。

【第 1 步:创建全局取消标志】

打开或新建一个静态类文件(例如 GlobalCancel.cs)。

粘贴以下代码:

---------------------------------------- 复制开始 ----------------------------------------

using System;

namespace DeepNestLib

{

public static class GlobalCancel

{

// ★★★ 核心:volatile 保证多线程可见性 ★★★

public static volatile bool IsRequested = false;

// ★★★ 每次命令开始必须调用 ★★★

public static void Reset() => IsRequested = false;

}

}

---------------------------------------- 复制结束 ----------------------------------------

新手常见错误:忘记加 volatile。如果不加,Release 模式下 CPU 缓存会导致循环永远看不到标志变化。

【第 2 步:修改进度窗体(ProgressForm)】

找到你的 ProgressForm 类,确保取消按钮的 Click 事件包含以下内容:

---------------------------------------- 复制开始 ----------------------------------------

// 在 ProgressForm 类中,必须有一个公共属性

public bool IsCancelled { get; private set; } = false;

// 在取消按钮的 Click 事件中

this.btnCancel.Click += (s, e) =>

{

this.IsCancelled = true;

this.lblMessage.Text = "正在取消排版,请稍候...";

// ★★★ 强制刷新界面(非常重要!) ★★★

this.Refresh();

Application.DoEvents();

};

---------------------------------------- 复制结束 ----------------------------------------

新手常见错误:只设置了 IsCancelled = true,但没有调用 this.Refresh() 和 Application.DoEvents()。

界面看起来像"卡死",实际已经点中了,但界面没刷新。

【第 3 步:修改命令入口,重置标志】

找到你的 CommandMethod 入口函数,在最开头加上:

---------------------------------------- 复制开始 ----------------------------------------

CommandMethod("XX")

public void RunCommand()

{

// ★★★ 第一件事:重置全局取消标志 ★★★

GlobalCancel.Reset();

_cancelRequested = false;

// ... 然后才是业务代码 ...

// ... 最后调用异步函数 ...

}

---------------------------------------- 复制结束 ----------------------------------------

新手常见错误:只在异步函数开头重置,没有在命令入口重置。

如果上一次排版中途取消了,下一次运行命令时,异步函数还没执行到重置代码,

后台任务就已经被之前的取消标志"杀死"了,导致"一运行就自动取消"。

【第 4 步:修改核心异步编排函数】

找到你的 RunDirectNestingRectAsync(或类似名称),按照以下模板修改。

---------------------------------------- 复制开始 ----------------------------------------

private void RunDirectNestingRectAsync(Database db, Editor ed, List<PartData> parts, Point2d basePoint)

{

// ========================================================================

// 第一部分:初始化(重置标志 + 清空消息队列)

// ========================================================================

// 1. 重置取消标志(再次确保)

GlobalCancel.Reset();

_cancelRequested = false;

// 2. 清空旧的消息队列

while (_messageQueue.TryDequeue(out _)) { }

// 3. 创建并显示进度窗体

var progress = new ProgressForm();

progress.SetMessage("正在初始化...");

progress.Show();

Application.DoEvents();

// 4. 定义共享数据容器(线程安全需加锁)

var placed = new List<PlacementResult>();

var sheets = new List<SheetData>();

int sheetIndex = 0;

int totalPlaced = 0;

bool isFinished = false;

string errorMsg = null;

// ========================================================================

// 第二部分:启动后台计算任务(Task.Run)

// ========================================================================

var backgroundTask = Task.Run(() =>

{

try

{

var localParts = allParts;

while (localParts.Count > 0 && sheetIndex < 1000)

{

// ★★★ 每次循环开始检查取消标志 ★★★

if (GlobalCancel.IsRequested || _cancelRequested)

break;

// ... 你的业务计算代码 ...

for (int iter = 0; iter < MaxIterations; iter++)

{

// ★★★ 每次迭代前检查 ★★★

if (GlobalCancel.IsRequested || _cancelRequested)

break;

// ... 业务计算 ...

// ★★★ 更新进度(跨线程 + 异常保护) ★★★

try

{

progress.Invoke((MethodInvoker)delegate

{

progress.UpdateProgress(iter + 1, MaxIterations);

});

}

catch (ObjectDisposedException) { break; }

catch (InvalidOperationException) { break; }

}

if (GlobalCancel.IsRequested || _cancelRequested)

break;

// ... 提取结果 ...

// ★★★ 日志入队(禁止直接 ed.WriteMessage) ★★★

_messageQueue.Enqueue($"\n母材 #{sheetIndex + 1} 放置 {placedCount} 个");

sheetIndex++;

}

// ★★★ 无论正常结束还是取消,都要标记完成 ★★★

isFinished = true;

}

catch (Exception ex)

{

errorMsg = ex.Message;

isFinished = true; // ★★★ 异常也标记完成 ★★★

}

});

// ========================================================================

// 第三部分:启动 UI 轮询定时器(200ms)

// ========================================================================

_pollTimer = new System.Windows.Forms.Timer();

_pollTimer.Interval = 200;

_pollTimer.Tick += (s, e) =>

{

// 1. 输出消息队列中的日志

while (_messageQueue.TryDequeue(out string msg))

ed.WriteMessage(msg);

// ================================================================

// ★★★ 取消逻辑:检测到用户点击了取消按钮 ★★★

// ================================================================

if (progress.IsCancelled)

{

// a) 通知后台线程停止

GlobalCancel.IsRequested = true;

_cancelRequested = true;

// b) 立即输出取消信息

ed.WriteMessage($"\n✅ 排版已取消,共使用 {sheetIndex} 块母材,已放置 {totalPlaced} 个零件。");

// c) ★★★ 立即关闭进度窗体(不等待后台任务) ★★★

progress.Close();

// d) 停止定时器

_pollTimer.Stop();

_pollTimer = null;

return;

}

// 检查后台任务是否完成

if (backgroundTask.IsCompleted || backgroundTask.IsFaulted)

{

_pollTimer.Stop();

if (errorMsg != null)

ed.WriteMessage($"\n错误: {errorMsg}");

else if (isFinished && !progress.IsCancelled)

{

ed.WriteMessage($"\n排版结束,共使用 {sheetIndex} 块母材,成功放置 {totalPlaced} 个零件。");

// 绘图(加锁保护)

List<SheetData> sheetCopy;

List<PlacementResult> placedCopy;

lock (sheets) { sheetCopy = new List<SheetData>(sheets); }

lock (placed) { placedCopy = new List<PlacementResult>(placed); }

DrawResult(db, ed, placedCopy, sheetCopy);

}

progress.Close();

_pollTimer = null;

}

};

_pollTimer.Start();

}

---------------------------------------- 复制结束 ----------------------------------------

新手常见错误 1:在取消分支里没有立即关闭窗体,而是 return 等待后台任务。

必须:检测到取消 → 立即 Close()。

新手常见错误 2:在后台任务中直接写 ed.WriteMessage。

必须:用 _messageQueue.Enqueue() 入队。

【第 5 步:在深层业务逻辑中添加取消检查】

在 Background.placeParts、NestingContext.NestIterate、SvgNest.launchWorkers 中,

所有循环入口添加检查:

---------------------------------------- 复制开始 ----------------------------------------

// Background.placeParts

public static SheetPlacement placeParts(...)

{

if (GlobalCancel.IsRequested) return null;

while (parts.Length > 0)

{

if (GlobalCancel.IsRequested) return null;

for (int i = 0; i < parts.Length; i++)

{

if (GlobalCancel.IsRequested) break;

// ... 业务代码 ...

}

}

return result;

}

// NestingContext.NestIterate

public void NestIterate()

{

if (GlobalCancel.IsRequested) return;

// ... 拷贝数据 ...

if (GlobalCancel.IsRequested) return;

Nest.launchWorkers(partsLocal.ToArray());

}

---------------------------------------- 复制结束 -----------------------------------------

═══════════════════════════════════════════════════════════════════════════════

第四部分:完整代码骨架(5 个关键文件)

═══════════════════════════════════════════════════════════════════════════════

【文件 1:GlobalCancel.cs】

─────────────────────────────────────────────────────────────────────────────

using System;

namespace DeepNestLib

{

public static class GlobalCancel

{

public static volatile bool IsRequested = false;

public static void Reset() => IsRequested = false;

}

}

【文件 2:ProgressForm.cs(节选)】

─────────────────────────────────────────────────────────────────────────────

using System;

using System.Windows.Forms;

public class ProgressForm : Form

{

private Button btnCancel;

private Label lblMessage;

private ProgressBar progressBar;

public bool IsCancelled { get; private set; } = false;

private void InitializeComponent()

{

this.btnCancel = new Button();

this.lblMessage = new Label();

// ... 其他控件初始化 ...

this.btnCancel.Text = "取消排版";

this.btnCancel.Click += (s, e) =>

{

this.IsCancelled = true;

this.lblMessage.Text = "正在取消排版,请稍候...";

this.Refresh();

Application.DoEvents();

};

}

public void SetMessage(string msg)

{

this.lblMessage.Text = msg;

Application.DoEvents();

}

public void UpdateProgress(int current, int total)

{

if (total == 0) return;

int percent = (int)((double)current / total * 100);

this.progressBar.Value = percent;

this.progressBar.Refresh();

Application.DoEvents();

}

}

【文件 3:Background.cs(节选)】

─────────────────────────────────────────────────────────────────────────────

namespace DeepNestLib

{

public class Background

{

public static SheetPlacement placeParts(NFP\[\] sheets, NFP\[\] parts,

SvgNestConfig config, int nestindex)

{

if (GlobalCancel.IsRequested) return null;

while (parts.Length > 0)

{

if (GlobalCancel.IsRequested) return null;

for (int i = 0; i < parts.Length; i++)

{

if (GlobalCancel.IsRequested) break;

// ... 业务逻辑 ...

}

}

return result;

}

public void BackgroundStart(DataInfo data)

{

if (GlobalCancel.IsRequested) return;

// ... 启动计算 ...

}

public void sync()

{

if (GlobalCancel.IsRequested) return;

var placement = placeParts(...);

if (placement == null || GlobalCancel.IsRequested) return;

ResponseAction(placement);

}

}

}

【文件 4:NestingContext.cs(节选)】

─────────────────────────────────────────────────────────────────────────────

public class NestingContext

{

public void NestIterate()

{

if (GlobalCancel.IsRequested) return;

// ... 拷贝数据 ...

if (GlobalCancel.IsRequested) return;

Nest.launchWorkers(partsLocal.ToArray());

}

}

【文件 5:SvgNest.cs(节选)】

─────────────────────────────────────────────────────────────────────────────

public class SvgNest

{

public void launchWorkers(NestItem\[\] parts)

{

if (GlobalCancel.IsRequested) return;

// ... 准备数据 ...

if (GlobalCancel.IsRequested) return;

background.BackgroundStart(data);

}

}

═══════════════════════════════════════════════════════════════════════════════

第五部分:常见错误与排查方法

═══════════════════════════════════════════════════════════════════════════════

错误 1:点击取消后窗口没关闭,但命令行有取消信息

原因:定时器收尾分支中,progress.Close() 被异常跳过。

排查:检查 progress.Close() 之前的代码是否抛出了异常(如 DrawResult 中绘图失败)。

解决:把 progress.Close() 放在 try-finally 中,或放在取消分支中先执行关闭再绘图。

错误 2:点击取消后窗口关闭了,但后台任务还在疯狂跑 CPU

原因:后台任务的循环中没有正确检查取消标志,或者检查点离得太远。

排查:在循环入口添加日志,确认是否进入了循环。

解决:在所有 while、for、foreach 入口添加检查。

错误 3:第二次运行排版时,还没点击取消就自动取消了

原因:全局取消标志没有重置,保留着上一次的 true 值。

排查:在命令入口和异步函数开头添加日志,输出 GlobalCancel.IsRequested 的值。

解决:在命令入口处强制重置为 false。

错误 4:跨线程更新进度条时程序崩溃

原因:进度窗体已被关闭,但后台线程仍在调用 progress.Invoke。

排查:查看异常类型(通常是 ObjectDisposedException)。

解决:用 try-catch 包裹 Invoke,捕获这两种异常并 break。

错误 5:定时器 Tick 中访问 placed 或 sheets 时出现异常

原因:后台线程正在修改这些集合,同时定时器在读取。

排查:检查是否在读取时加了 lock。

解决:在读取和写入时都使用 lock 保护。

错误 6:取消后命令行没有输出"排版已取消"

原因:定时器检测到 progress.IsCancelled 为 false。

排查:在定时器 Tick 开头添加日志,输出 progress.IsCancelled 的值。

解决:检查取消按钮的 Click 事件是否正确设置了 IsCancelled = true。

错误 7:AutoCAD 报"eLockViolation"错误

原因:在操作数据库对象时没有锁定文档,或事务嵌套错误。

排查:检查所有数据库操作是否都在 using (DocumentLock) 和 using (Transaction) 中。

解决:确保 DrawResult 等函数中用 using (DocumentLock docLock = doc.LockDocument())。

错误 8:原生 DLL 无法中断(minkowski.dll 等)

原因:C# 代码无法中断正在执行的 C++ 原生代码。

解决:这是无法绕过的限制,只能等待当前调用返回。可在文档中说明。

═══════════════════════════════════════════════════════════════════════════════

第六部分:新手最容易忽略的 3 个致命细节(一定要看)

═══════════════════════════════════════════════════════════════════════════════

致命细节 1:静态变量未重置

现象:第二次运行命令,还没点取消就自动停止。

原因:上一次取消后 GlobalCancel.IsRequested 一直为 true。

解决:在 CommandMethod 入口函数的第一行调用 GlobalCancel.Reset()。

致命细节 2:取消时没有立即关闭窗体

现象:点了取消,命令行输出了取消信息,但进度窗体一直不关。

原因:在定时器的取消分支中,只设置了标志,没有调用 progress.Close()。

解决:在取消分支中,设置标志后立即调用 progress.Close()。

致命细节 3:后台任务中直接操作 UI 或 AutoCAD Editor

现象:运行时报"调用线程无法访问此对象"或"eLockViolation"。

原因:后台线程不能直接操作 UI 控件或 ed.WriteMessage。

解决:UI 用 Invoke,日志用消息队列,数据库操作用 DocumentLock。

═══════════════════════════════════════════════════════════════════════════════

第七部分:快速参考卡片(打印贴在工位)

═══════════════════════════════════════════════════════════════════════════════

【取消标志定义】

public static volatile bool IsRequested = false;

【取消标志重置】

GlobalCancel.IsRequested = false;

【取消标志检查】

if (GlobalCancel.IsRequested) break;

【取消标志设置】

GlobalCancel.IsRequested = true;

【消息队列定义】

private static ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();

【消息入队(后台线程)】

_messageQueue.Enqueue("消息");

【消息出队(UI 线程)】

while (_messageQueue.TryDequeue(out string msg)) { ed.WriteMessage(msg); }

【跨线程更新 UI】

try { progress.Invoke((MethodInvoker)delegate { progress.UpdateProgress(...); }); }

catch (ObjectDisposedException) { break; }

catch (InvalidOperationException) { break; }

【定时器创建与启动】

_pollTimer = new System.Windows.Forms.Timer();

_pollTimer.Interval = 200;

_pollTimer.Tick += (s, e) => { ... };

_pollTimer.Start();

【定时器停止与释放】

_pollTimer.Stop();

_pollTimer = null;

【取消立即响应(定时器 Tick 中)】

if (progress.IsCancelled)

{

GlobalCancel.IsRequested = true;

ed.WriteMessage("✅ 已取消");

progress.Close();

_pollTimer.Stop();

_pollTimer = null;

return;

}

═══════════════════════════════════════════════════════════════════════════════

第八部分:如何用测试代码验证(不用跑完整排样)

═══════════════════════════════════════════════════════════════════════════════

如果你不想跑复杂的排样,只想验证"取消框架"是否搭建成功,用 Thread.Sleep 模拟耗时任务:

---------------------------------------- 复制开始 ----------------------------------------

// 在后台任务中替换为以下测试代码

for (int i = 0; i < 100; i++)

{

if (GlobalCancel.IsRequested) break;

Thread.Sleep(100); // 模拟耗时 100ms

progress.Invoke((MethodInvoker)delegate

{

progress.UpdateProgress(i + 1, 100);

});

}

---------------------------------------- 复制结束 ----------------------------------------

验证标准:

点击取消后,进度条能在 1 秒内停止并关闭窗体 → 取消框架搭建成功。

═══════════════════════════════════════════════════════════════════════════════

第九部分:最终检查清单(上线前验证)

═══════════════════════════════════════════════════════════════════════════════

□ 1. 所有耗时循环都有取消检查点(while、for、foreach)

□ 2. 取消时窗体能在 1 秒内关闭

□ 3. 取消后命令行有明确的"已取消"提示

□ 4. 取消后后台任务能在下一次检查点退出(观察 CPU 占用下降)

□ 5. 取消后再次运行排版,不会自动取消(标志已重置)

□ 6. 连续多次取消再运行,程序稳定不崩溃

□ 7. 正常完成排版,不会输出"已取消"的误报

□ 8. 绘图结果正确,没有因为取消而残留错误图形

□ 9. 所有数据库事务正确提交或回滚,没有锁残留

□ 10. 进度条数字正确,不会因为取消而显示异常值

═══════════════════════════════════════════════════════════════════════════════

第十部分:文件修改优先级表

═══════════════════════════════════════════════════════════════════════════════

优先级 1(必须改):

Background.cs - 添加取消标志和循环检查

NestingContext.cs - 迭代入口添加快速失败

SvgNest.cs - Worker 启动前检查取消标志

优先级 2(核心改):

AAAdn排版net48支持凸度.cs - 异步编排 + 定时器 + 消息队列

优先级 3(界面改):

NestConfigDialog.cs - 进度窗体取消按钮

优先级 4(新建):

GlobalCancel.cs - 全局取消标志(如果没有的话)

═══════════════════════════════════════════════════════════════════════════════

第十一部分:总结

═══════════════════════════════════════════════════════════════════════════════

通过 协作式取消 + 后台线程 + UI 定时器 + 消息队列 的组合模式,

可以在任何大型 .NET 项目中实现"点击取消立即响应"的用户体验。

核心只有 4 个步骤:

  1. 定义全局取消标志。

  2. 在耗时循环中主动检查。

  3. 使用后台线程 + 定时器分离 UI 和计算。

  4. 取消时立即关闭 UI,不等待后台。

记住一句话:"让计算主动让路,而不是让用户等待。"

这套方案已在 AutoCAD 排样插件中成功实施,有效提升了用户体验,

且代码可维护性良好。对于包含第三方原生库的场景,可进一步考虑

进程隔离方案,但多数情况下上述模式已足够。

================================================================================

文档结束

================================================================================

相关推荐
岁月如歌77861 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构
XiaQ09192 小时前
Linux 常用命令学习笔记(用户管理篇):用户、组与权限的钥匙
linux·开发语言·笔记·学习
不可求~2 小时前
C++ 报错交给 AI 之前,先准备好这 8 类信息
开发语言·c++·人工智能
名字还没想好☜2 小时前
用 Python struct 解析二进制协议:pack/unpack、字节序与对齐踩坑
开发语言·python·二进制·struct
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(四十六):改了十次规则,AI员工还是老样子——会话缓存的坑
java·spring·缓存
一知半解仙2 小时前
当机器人学会泛化,而我在写Java:一名后端开发者的2026年8月21日观察手记
java·开发语言·机器人
青 春 记 忆2 小时前
LeetCode 155. 最小栈|Python 解法详解
开发语言·python·leetcode
葡萄成熟时 !10 小时前
JAVA 常用API学习笔记
java·笔记·学习
剩下了什么11 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang