You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中用Dart将stdout重定向至文本框(含C++ DLL/FFI场景)

实现Flutter GUI捕获Dart和C++ DLL的stdout输出

一、捕获Dart自身的stdout输出

Dart的stdout是可自定义输出目标的IOSink,通过替换它的输出载体就能捕获所有Dart层面的控制台输出:

  1. 实现自定义输出接收器,用于收集内容并通知UI更新:
class ConsoleOutputSink implements StringSink {
  final void Function(String) onOutput;

  ConsoleOutputSink(this.onOutput);

  @override
  void write(Object? object) {
    final text = object.toString();
    onOutput(text);
  }

  @override
  void writeAll(Iterable objects, [String separator = '']) {
    final text = objects.join(separator);
    onOutput(text);
  }

  @override
  void writeln([Object? object = '']) {
    final text = object.toString() + '\n';
    onOutput(text);
  }
}
  1. 在Flutter页面的State类中初始化接收器并重定向stdout:
late ConsoleOutputSink _outputSink;
final TextEditingController _consoleController = TextEditingController();

@override
void initState() {
  super.initState();
  _outputSink = ConsoleOutputSink((text) {
    if (mounted) {
      setState(() {
        _consoleController.text += text;
        // 自动滚动到文本末尾
        _consoleController.selection = TextSelection.fromPosition(
          TextPosition(offset: _consoleController.text.length),
        );
      });
    }
  });
  // 替换默认stdout为自定义接收器
  stdout = _outputSink as IOSink;
}

@override
void dispose() {
  // 恢复系统默认stdout
  stdout = stdout;
  _consoleController.dispose();
  super.dispose();
}
  1. UI层用只读文本框展示输出:
TextField(
  controller: _consoleController,
  maxLines: null,
  readOnly: true,
  decoration: const InputDecoration(
    border: OutlineInputBorder(),
    labelText: '控制台输出',
  ),
)

此后Dart的print()、stdout.write()等输出都会被捕获到文本框中。

二、捕获C++ DLL的stdout输出

C++ DLL的默认输出目标是系统控制台,需根据是否能修改DLL代码选择不同方案:

方案1:修改DLL代码(推荐,更可靠)

直接给DLL添加回调接口,让C++输出通过回调传递给Dart,而非依赖stdout:

// DLL头文件
extern "C" __declspec(dllexport) void setOutputCallback(void (*callback)(const char*));
extern "C" __declspec(dllexport) void dllDemoFunction();

// DLL实现代码
#include <iostream>
#include <cstring>

void (*outputCallback)(const char*) = nullptr;

void setOutputCallback(void (*callback)(const char*)) {
    outputCallback = callback;
}

void customPrint(const char* msg) {
    if (outputCallback) {
        outputCallback(msg);
    } else {
        // 无回调时回退到控制台输出
        std::cout << msg;
    }
}

void dllDemoFunction() {
    customPrint("C++ DLL输出内容\n");
}

在Dart中通过FFI注册回调并接收输出:

// 定义回调类型
typedef OutputCallback = Void Function(Pointer<Utf8>);
typedef OutputCallbackNative = Void Function(Pointer<Utf8>);

// 加载DLL
final dylib = DynamicLibrary.open('your_dll_path.dll');

// 注册Dart回调到DLL
final setOutputCallback = dylib.lookupFunction<
    Void Function(Pointer<NativeFunction<OutputCallbackNative>>),
    Void Function(Pointer<NativeFunction<OutputCallback>>)
>('setOutputCallback');

final nativeCallback = NativeCallable<OutputCallbackNative>.isolateLocal((msg) {
  final text = msg.toDartString();
  if (mounted) {
    setState(() {
      _consoleController.text += text;
      _consoleController.selection = TextSelection.fromPosition(
        TextPosition(offset: _consoleController.text.length),
      );
    });
  }
});

setOutputCallback(nativeCallback.nativeFunction);

// 调用DLL方法触发输出
final dllDemoFunction = dylib.lookupFunction<Void Function(), Void Function()>('dllDemoFunction');
dllDemoFunction();

// 页面销毁时释放回调资源
@override
void dispose() {
  nativeCallback.close();
  super.dispose();
}

方案2:无法修改DLL时,通过Windows API重定向stdout

利用Windows管道API将C++的stdout重定向到管道,再由Dart读取管道内容:

// 定义Windows API函数类型
typedef CreatePipeFunc = Bool Function(
  Pointer<Handle>, Pointer<Handle>, Pointer<Void>, Uint32,
);
typedef CreatePipe = bool Function(
  Pointer<int>, Pointer<int>, Pointer<void>, int,
);

typedef SetStdHandleFunc = Bool Function(Uint32, Handle);
typedef SetStdHandle = bool Function(int, int);

typedef ReadFileFunc = Bool Function(
  Handle, Pointer<Uint8>, Uint32, Pointer<Uint32>, Pointer<Void>,
);
typedef ReadFile = bool Function(
  int, Pointer<Uint8>, int, Pointer<int>, Pointer<void>,
);

const int STD_OUTPUT_HANDLE = -11;

// 加载Windows系统库
final kernel32 = DynamicLibrary.open('kernel32.dll');
final createPipe = kernel32.lookupFunction<CreatePipeFunc, CreatePipe>('CreatePipe');
final setStdHandle = kernel32.lookupFunction<SetStdHandleFunc, SetStdHandle>('SetStdHandle');
final readFile = kernel32.lookupFunction<ReadFileFunc, ReadFile>('ReadFile');

@override
void initState() {
  super.initState();
  _setupPipeRedirection();
}

void _setupPipeRedirection() {
  final readPipe = calloc<Handle>();
  final writePipe = calloc<Handle>();
  // 创建匿名管道
  createPipe(readPipe, writePipe, nullptr, 0);
  // 重定向stdout到管道写端
  setStdHandle(STD_OUTPUT_HANDLE, writePipe.value);

  // 创建SendPort用于跨线程通知UI更新
  final receivePort = ReceivePort();
  receivePort.listen((text) {
    if (mounted) {
      setState(() {
        _consoleController.text += text;
        _consoleController.selection = TextSelection.fromPosition(
          TextPosition(offset: _consoleController.text.length),
        );
      });
    }
  });
  // 启动独立线程读取管道内容
  Isolate.spawn(_readPipeContent, [readPipe.value, receivePort.sendPort]);
}

void _readPipeContent(List args) {
  final pipeHandle = args[0] as int;
  final sendPort = args[1] as SendPort;
  final buffer = calloc<Uint8>(1024);
  final bytesRead = calloc<Uint32>();

  while (true) {
    // 读取管道数据
    final success = readFile(pipeHandle, buffer, 1024, bytesRead, nullptr);
    if (!success || bytesRead.value == 0) break;
    final text = utf8.decode(buffer.asTypedList(bytesRead.value));
    sendPort.send(text);
  }

  calloc.free(buffer);
  calloc.free(bytesRead);
}

三、关键注意事项

  • 所有UI更新必须在Flutter主线程执行,捕获到输出后需通过setState或SchedulerBinding.instance.addPostFrameCallback触发更新。
  • 使用C++回调时,必须在页面销毁时关闭NativeCallable,避免内存泄漏。
  • 管道重定向方案仅适用于Windows平台,跨平台场景优先选择回调方案。

内容的提问来源于stack exchange,提问作者BND

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 21:21:49