当前位置: 首页 > news >正文

C#实现控制台多区域输出

近一年以来,AI Agent的发展速度非常快。
如果经常使用一些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 还提供了一组非常重要的API:

Console.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, "[ 运行日志 (滚动) ]"); }

运行之后界面如下:

整个布局没有使用任何第三方组件。
本质上就是利用字符绘制边框。

实现系统时间区域#

布局完成之后,实现左上角的时间显示区域。
代码如下:

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<LogEntry> _logQueue = new Queue<LogEntry>();

新增日志:

_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); } }

后续所有区域输出都通过该方法完成。
这样能够保证同一时刻只有一个线程修改控制台状态。
避免多个线程抢占光标位置导致界面错乱。

优雅退出机制#
http://www.jsqmd.com/news/1087944/

相关文章:

  • 换手机之后,所有平台的二次验证码怎么一次性恢复
  • 正则表达式在SQL注入防护中的精准应用与实战策略
  • XSS漏洞攻防实战:从原理到靶场实践与防御策略
  • 一文读懂sysmaster的1+1+N架构:核心组件与插件化设计详解
  • 近期初学量化选工具,先按阶段看任务模块
  • AI赋能JMeter+Jenkins自动化测试:智能脚本生成与结果分析实战
  • VCSA证书过期实战:从报错诊断到一键续订的完整指南
  • D2DX:终极免费方案!让经典《暗黑破坏神2》在现代PC上完美运行
  • RA8T2 ADC16H寄存器实战:从状态机到驱动代码的避坑指南
  • Java反序列化漏洞实战:从CTF靶场到ysoserial利用链深度解析
  • 网盘直链下载助手完全指南:无需客户端轻松下载八大网盘文件
  • 3种场景,1个工具:Video2X如何让AI视频增强变得简单实用
  • FakeLocation位置模拟终极指南:如何在Android设备上实现精准定位伪装?
  • VisionMaster 实战解析:线线测量在精密尺寸检测中的应用
  • 高效液冷:数据中心散热新选择
  • 信息学奥赛经典题解:小球下落(drop)的二叉树模拟与优化
  • 3分钟解锁QQ音乐加密文件:qmcdump无损转换工具完全指南
  • RA8T2 ADC16H自校准与自诊断功能详解与实战配置
  • SolidWorks工程图实战:从零到一掌握公差标注的正确姿势
  • OCAuxiliaryTools:可视化OpenCore配置,让黑苹果安装变得简单高效
  • 【AUTOSAR】VCU 软件平台化架构设计解析 —— 从硬件抽象到应用层集成
  • UE4SS终极指南:5步打造完美虚幻引擎游戏Mod环境
  • Java SpringBoot+Vue3+MyBatis 招聘系统系统源码|前后端分离+MySQL数据库
  • PartKeepr:电子工程师的终极开源库存管理解决方案
  • 如何用nunif iw3将2D视频转换为沉浸式3D VR体验:终极完整指南
  • 拉泽替尼Lazertinib与阿美替尼横向比较,三代EGFR-TKI耐药后如何选
  • UnifiedBus资源全局调度:如何实现异构硬件动态组合扩展
  • 终极解决方案!VisualCppRedist AIO:一键修复所有Windows DLL缺失错误
  • 事业单位技术岗晋升困局(软考证书未激活职称效力?)——基于全国27家单位HR访谈的稀缺数据报告
  • 科学大模型的可信边界:从Galactica失败看数据洁癖与符号一致性