如何在C# WinForms中获取玩家下一步走法且不阻塞UI
国际象棋Windows Forms程序中HumanPlayer.GetMove()方法的实现问题
我正在用C# Windows Forms编写一款国际象棋程序,需要在自定义的HumanPlayer类中实现GetMove()方法——该方法要通过玩家点击棋盘UI上的两个不同格子,返回一个ChessMove对象。希望能得到实现建议,若我有概念误解也请指出。
尝试的同步代码实现
class HumanPlayer : Player { private Coords _selected; public HumanPlayer(PieceColour colour) : base(colour) { _selected = new Coords(); } public override ChessMove GetMove(Board board) { Coords Start = new Coords(); board.RaiseSquareClicked += ReceiveStartSquareClickInfo; // 想要等待事件触发后再继续执行 Start = _selected; board.RaiseSquareClicked -= ReceiveStartSquareClickInfo; Coords End = new Coords(); board.RaiseSquareClicked += ReceiveEndSquareClickInfo; // 想要等待事件触发后再继续执行 End = _selected; board.RaiseSquareClicked -= ReceiveEndSquareClickInfo; return new ChessMove(Start, End); } public void ReceiveStartSquareClickInfo(object sender, SquareClickedEventArgs e) { _selected = e.Square.Coords; } public void ReceiveEndSquareClickInfo(object sender, SquareClickedEventArgs e) { _selected = e.Square.Coords; } }
这段代码的核心问题:注册事件后立刻读取_selected,此时玩家尚未点击,_selected还是初始值,完全达不到等待点击的效果。
尝试异步实现时遇到的问题
我试过用AutoResetEvent.WaitOne()和Set(),但直接导致UI停止响应——因为它阻塞了UI线程,事件根本无法触发。
之后尝试用async/await,但越弄越乱,代码无法正常工作:
public async void Play() { _currentPlayer = _players[0]; _currentTurn = 1; while (!GameOver()) { ChessMove move= await _currentPlayer.GetMove(_board); if (_currentPlayer == _players[1]) { _currentTurn += 1; _currentPlayer = _players[0]; } else { _currentPlayer = _players[1]; } } } class HumanPlayer : Player { private Coords _selected; private TaskCompletionSource<bool> _squareClicked; public HumanPlayer(PieceColour colour) : base(colour) { _selected = new Coords(); } public override async Task<ChessMove> GetMove(Board board) { Coords Start = new Coords(); _squareClicked = new TaskCompletionSource<bool>(); board.RaiseSquareClicked += ReceiveStartSquareClickInfo; _squareClicked.Task.Wait(); Start = _selected; board.RaiseSquareClicked -= ReceiveStartSquareClickInfo; Coords End = new Coords(); _squareClicked = new TaskCompletionSource<bool>(); board.RaiseSquareClicked += ReceiveEndSquareClickInfo; _squareClicked.Task.Wait(); End = _selected; board.RaiseSquareClicked -= ReceiveEndSquareClickInfo; return new ChessMove(Start, End); } public async void ReceiveStartSquareClickInfo(object sender, SquareClickedEventArgs e) { _squareClicked.SetResult(true); _selected = e.Square.Coords; } public async void ReceiveEndSquareClickInfo(object sender, SquareClickedEventArgs e) { _squareClicked.SetResult(true); _selected = e.Square.Coords; } }
这段代码的核心错误:
- 用
_squareClicked.Task.Wait()阻塞了UI线程,和AutoResetEvent的问题一样,UI卡死,事件无法触发。 - 用
TaskCompletionSource<bool>需要额外维护_selected字段,冗余且容易出错。 - 事件处理方法用
async void完全没必要,还可能导致未捕获异常。
正确实现方案及概念讲解
核心概念:async/await与UI线程
Windows Forms的UI操作必须在UI线程执行,await的作用是:当等待的任务未完成时,释放UI线程让它处理用户交互(比如点击),任务完成后再回到原线程继续执行后续代码。绝对不能用Task.Wait()或Task.Result阻塞UI线程,否则会导致UI卡死。
修正后的代码实现
我们可以用TaskCompletionSource<Coords>直接捕获点击的坐标,不需要额外的字段:
// 修正后的Play方法(async void只适合UI事件处理,这里改成async Task更规范) public async Task Play() { _currentPlayer = _players[0]; _currentTurn = 1; while (!GameOver()) { ChessMove move = await _currentPlayer.GetMove(_board); // 执行移动逻辑... // 切换玩家 _currentPlayer = _currentPlayer == _players[0] ? _players[1] : _players[0]; if (_currentPlayer == _players[0]) _currentTurn += 1; } } class HumanPlayer : Player { public HumanPlayer(PieceColour colour) : base(colour) { } public override async Task<ChessMove> GetMove(Board board) { // 等待第一个点击(起始格子) var startCoords = await WaitForSquareClick(board); // 等待第二个点击(目标格子) var endCoords = await WaitForSquareClick(board); return new ChessMove(startCoords, endCoords); } // 封装等待单个格子点击的逻辑 private Task<Coords> WaitForSquareClick(Board board) { var tcs = new TaskCompletionSource<Coords>(); // 定义事件处理方法 void Handler(object sender, SquareClickedEventArgs e) { // 触发后立即注销事件,避免重复触发 board.RaiseSquareClicked -= Handler; // 设置任务结果,结束等待 tcs.SetResult(e.Square.Coords); } // 注册事件 board.RaiseSquareClicked += Handler; // 返回任务,等待点击 return tcs.Task; } }
代码说明
- 封装等待逻辑:把等待单个格子点击的逻辑抽成
WaitForSquareClick方法,代码更简洁复用。 - TaskCompletionSource
:直接将点击的坐标作为任务结果,不需要额外维护状态字段。 - 事件注销:在事件触发后立即注销,避免后续点击再次触发该处理方法,同时防止内存泄漏。
- 避免阻塞:全程用
await等待任务,不会阻塞UI线程,玩家点击时UI能正常响应。
额外注意事项
- 如果需要判断玩家点击的起始格子是否属于自己的棋子,可以在
Handler方法里先校验,不符合条件就不设置TaskCompletionSource的结果,让玩家重新点击。 - 可以给
WaitForSquareClick添加超时逻辑(比如玩家长时间不点击自动结束回合),用Task.Delay和Task.WhenAny实现。
内容的提问来源于stack exchange,提问作者Shinglington
相关产品推荐
相关产品推荐

