如果经常使用一些Agent CLI工具例如 Claude Code、Gemini CLI、OpenCode 等产品会发现它们有一个共同特点虽然运行在终端之中但已经完全不是传统命令行程序的样子。在执行任务过程中它们通常会同时展示Agent执行状态思考过程文件变更信息Token统计系统日志工具调用结果整个终端界面被划分成多个独立区域并且每个区域都在实时刷新。例如下面这种布局┌────────────────────┬────────────────────┐│ Agent状态 │ Token统计 ││ │ │├────────────────────┴────────────────────┤│ ││ 执行过程区域 ││ │├─────────────────────────────────────────┤│ 系统日志 │└─────────────────────────────────────────┘上次在微信群里看到黑洞大佬在做类似的Agent CLI谈到过控制台多区域输出的问题我当时比较好奇C# 原生 Console 是如何实现多区域动态界面的呢经过一番研究之后发现实现原理并没有想象中复杂。本文通过一个简单示例介绍如何利用 C# Console 实现多区域布局动态内容刷新滚动日志窗口多线程安全输出优雅退出机制Console为什么能够实现多区域输出#大多数情况下我们使用 Console 都是这样Console.WriteLine(“任务开始”);for (int i 0; i 10; i){Console.WriteLine($“执行进度{i}”);}Console.WriteLine(“任务结束”);输出结果如下任务开始执行进度0执行进度1执行进度2…任务结束看起来控制台只能从上往下不断输出内容。实际上 Console 还提供了一组非常重要的APIConsole.SetCursorPosition(x, y);它允许程序直接控制光标位置。例如Console.SetCursorPosition(10, 5);Console.Write(“Hello”);程序会直接在指定坐标位置输出内容。也就是说Console ≠ 输出流而更像是Console 字符画布只要能够控制坐标位置就能够实现区域划分与动态刷新。这也是所有终端UI框架最基础的实现原理。实现控制台布局#首先需要将控制台划分成多个区域。本示例将控制台分成三个部分左上区域显示系统时间右上区域显示任务进度下半区域显示运行日志布局绘制代码如下static void DrawLayout(){int width Console.WindowWidth;int height Console.WindowHeight;int midX width / 2; int midY height / 2; for (int y 0; y midY; y) { SafeWrite(midX, y, │); } for (int x 0; x width - 1; x) { SafeWrite(x, midY, ─); } SafeWrite(2, 0, [ 系统时间 ]); SafeWrite(midX 2, 0, [ 任务进度 ]); SafeWrite(2, midY 1, [ 运行日志 (滚动) ]);}运行之后界面如下image整个布局没有使用任何第三方组件。本质上就是利用字符绘制边框。实现系统时间区域#布局完成之后实现左上角的时间显示区域。代码如下static void UpdateRegion_Clock(){while (_isRunning){SafeWrite(2,2,DateTime.Now.ToString(“yyyy-MM-dd HH:mm:ss”));Thread.Sleep(1000); }}运行效果2026-05-21 16:30:25由于始终输出到同一个位置因此每次刷新都会覆盖之前的内容。从而形成动态更新时间的效果。实现任务进度区域#右上角区域用于模拟任务进度。实现代码如下static void UpdateRegion_Progress(){int progress 0;int midX Console.WindowWidth / 2;while (_isRunning) { progress (progress 1) % 101; int barWidth 20; int filled (int)(barWidth * (progress / 100.0)); string bar [ new string(█, filled) new string( , barWidth - filled) $] {progress}%; SafeWrite(midX 2, 2, bar); Thread.Sleep(50); }}运行效果如下[██████████████ ] 72%这种实现方式和很多安装程序、下载工具中的进度条实现原理基本一致。实现滚动日志窗口#日志区域是整个示例最核心的部分。如果简单使用Console.WriteLine();日志会不断向下滚动。很快就会占满整个控制台。因此需要一个固定区域用于展示日志内容。首先定义日志队列private static readonly Queue_logQueue new Queue();新增日志_logQueue.Enqueue(new LogEntry{Text newLog,Color color});超过最大显示行数时移除旧日志while (_logQueue.Count _maxLogLines){_logQueue.Dequeue();}然后重新绘制日志区域foreach (var log in _logQueue){Console.SetCursorPosition(2, currentY);Console.ForegroundColor log.Color;Console.Write(log.Text);currentY;}运行效果如下15:32:11.212 [INFO ] 初始化完成15:32:11.518 [INFO ] 加载配置文件15:32:11.802 [DEBUG] 创建任务15:32:12.015 [WARN ] Token接近阈值15:32:12.381 [ERROR] 请求超时15:32:12.912 [INFO ] 自动重试成功同时根据日志等级设置不同颜色static ConsoleColor GetLogLevelColor(string level){switch (level){case “ERROR”:return ConsoleColor.Red;case “WARN”:return ConsoleColor.Yellow;case “DEBUG”:return ConsoleColor.DarkGray;default:return ConsoleColor.Green;}}这样整个日志区域看起来就更接近真实系统运行效果。多线程下的控制台竞争问题#到这里一个新的问题出现了。当前程序存在三个后台线程时间刷新线程进度刷新线程日志刷新线程这些线程都会同时操作控制台。例如Console.SetCursorPosition(x, y);Console.Write(text);如果多个线程同时执行很容易出现输出错乱。因此需要统一加锁。首先定义控制台锁对象private static readonly object _consoleLock new object();然后封装安全输出方法static void SafeWrite(int x,int y,string text){lock (_consoleLock){Console.SetCursorPosition(x, y);Console.Write(text);}}后续所有区域输出都通过该方法完成。这样能够保证同一时刻只有一个线程修改控制台状态。避免多个线程抢占光标位置导致界面错乱。优雅退出机制#很多控制台程序都会忽略退出逻辑。例如按下 CtrlC 之后程序被强制终止此时可能出现光标未恢复输出颜色异常界面残留