news 2026/11/2 2:06:02

VC++ MFC围棋程序:双缓冲绘图与消息驱动的Windows GUI实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++ MFC围棋程序:双缓冲绘图与消息驱动的Windows GUI实战

简介:这是一份面向VC++初学者与游戏编程爱好者的轻量级围棋对战程序源码,适用于Windows平台下的MFC桌面应用开发学习与实践。资源实现了双人本地对弈功能,支持15/17寸液晶屏适配窗口,下棋响应迅速,代码结构清晰,是理解MFC框架、GDI绘图、消息循环及简单游戏逻辑的典型入门案例。压缩包共24个文件,含7个头文件(.h)定义类接口与资源声明,6个实现文件(.cpp)涵盖视图、文档、主框架等核心模块,另有3个位图(.bmp)与2个图标(.ico)用于界面元素,辅以工程配置文件(.dsw/.dsp/.clw)和资源脚本(.rc/.rc2),完整呈现VC++ 6.0时代标准MFC项目组织方式。资源体积仅36KB,精炼易读,已获171人学习下载。读者可直接编译运行,深入剖析I_goDoc/I_goView等关键类的设计逻辑,掌握图形绘制、鼠标事件处理与棋盘状态管理等实用技能。

1. 可以双人对决的VC++围棋程序:一个被低估的MFC实战标本,专治“学完C++不会写界面”和“懂算法不会做交互”的双重焦虑

你写过几十个控制台版五子棋、井字棋,甚至手撸过AlphaBeta剪枝的AI下棋逻辑——但只要一碰图形界面,就卡在“怎么让鼠标点的位置变成棋盘坐标”“怎么重绘时不闪屏”“怎么保存对局状态又不崩掉窗口句柄”上。这个VC++围棋程序不是玩具,它是一套完整跑通的MFC工程:双人本地对战、落子合法性校验(气、提子、禁着点)、棋盘坐标映射、GDI双缓冲绘图、消息循环拦截、菜单与工具栏响应、.sgf对局文件读写——所有代码都在一个Visual Studio 2010/2015可编译的工程里,没有第三方库依赖,纯Win32+MFC。它适合两类人:刚啃完《C++ Primer》想落地练手的在校生,以及多年写后台服务、突然被派去维护老工业软件GUI模块的工程师。别被“围棋”吓住——它的核心是MFC事件驱动模型的典型闭环:WM_LBUTTONDOWN → ScreenToClient → 棋盘坐标换算 → 规则判断 → InvalidateRect → OnDraw → GDI绘图。这才是Windows桌面开发最硬核的肌肉记忆。


2. 工程结构与MFC消息机制:从App类到View类的控制流拆解

这个VC++围棋程序采用标准MFC SDI(单文档界面)架构,整个工程由CWinApp派生类、CDocument派生类、CView派生类三者构成。理解这三者的协作关系,是读懂所有交互逻辑的前提。很多初学者直接改OnDraw函数却不知道为什么点击没反应,根源在于没理清消息路由路径:用户点击触发的是CView的OnLButtonDown,而非CDocument或CWinApp。而棋盘状态数据实际存储在CDocument中,CView必须通过GetDocument()获取指针才能读写——这种分离设计正是MFC强制推行的“文档/视图”模式。

2.1 CChessApp:程序入口与资源注册

CChessApp继承自CWinApp,重载了InitInstance()。这里的关键操作有三处:
第一,调用AfxEnableControlContainer()启用ActiveX控件支持(虽然本程序未使用,但保留此调用是MFC向后兼容惯例);
第二,注册主框架窗口类:CSingleDocTemplate将CMainFrame、CChessDoc、CChessView三者绑定,确保新建文档时自动创建对应View;
第三,关键的一行:SetDialogBkColor(RGB(240,240,240), RGB(0,0,0))——它把对话框背景设为浅灰、文字设为黑色,直接影响后续CPropertyPage等控件的默认渲染色。若删除此行,在高DPI屏幕下可能出现文字模糊或背景发白问题。

// CChessApp::InitInstance() 中关键片段 CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CChessDoc), RUNTIME_CLASS(CMainFrame), // 主框架窗口 RUNTIME_CLASS(CChessView)); // 视图类 if (!pDocTemplate) return FALSE; AddDocTemplate(pDocTemplate);

提示:IDR_MAINFRAME是资源标识符,指向Resource.h中定义的#define IDR_MAINFRAME 128。它关联了菜单、工具栏、加速键三组资源。若你后续想添加“悔棋”按钮,必须在此IDR资源中新增工具栏按钮位图,并在CMainFrame::OnCreate()中调用DockControlBar(&m_wndToolBar)加载。

2.2 CChessDoc:棋盘状态的唯一真相源

CChessDoc继承自CDocument,所有落子、提子、胜负判定逻辑都发生在这里。它内部用二维数组m_board[19][19]存储棋盘状态(0=空,1=黑,2=白),并维护两个关键成员变量:m_nCurrentPlayer(当前轮到谁走,1或2)和m_bGameOver(是否终局)。注意:所有规则判断必须在CChessDoc中完成,CChessView只负责显示,绝不参与逻辑。这是MFC架构铁律,违反会导致状态不一致——比如CChessView自己判断“此处可落子”,但CChessDoc实际拒绝该操作,界面就显示错误。

CChessDoc提供核心接口:

  • BOOL CanPlaceStone(int x, int y, int player):检查(x,y)位置是否满足围棋规则(气、禁着点、打劫);
  • void PlaceStone(int x, int y, int player):执行落子、提子、更新气数;
  • int GetGameResult():返回0(进行中)、1(黑胜)、2(白胜)、3(和棋)。

这些函数全部在.cpp文件中实现,无任何外部依赖。例如CanPlaceStone内部调用私有函数CountLiberties(x, y)计算气数,再调用IsKoMove(x, y, player)检测打劫——所有围棋规则代码都在CChessDoc.cpp第217行开始,可逐行调试验证。

2.3 CChessView:GDI双缓冲绘图与坐标映射的实战场

CChessView继承自CView,承担全部可视化任务。它的OnDraw()函数是性能瓶颈所在,原程序采用传统GDI双缓冲技术规避闪烁:先在内存DC中绘制完整棋盘,再一次性BitBlt到屏幕DC。关键代码在OnDraw()开头:

void CChessView::OnDraw(CDC* pDC) { CChessDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // 创建内存DC用于双缓冲 CDC memDC; CBitmap bitmap; CRect rect; GetClientRect(&rect); memDC.CreateCompatibleDC(pDC); bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); CBitmap* pOldBitmap = memDC.SelectObject(&bitmap); // 在memDC中绘制棋盘、棋子、坐标线... DrawBoard(&memDC); DrawStones(&memDC); DrawCoordinates(&memDC); // 一次性拷贝到屏幕 pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }

参数说明:CreateCompatibleDC(pDC)创建与屏幕DC兼容的内存DC;CreateCompatibleBitmap()生成同尺寸位图;BitBlt(..., SRCCOPY)执行像素块拷贝。若你发现棋盘绘制缓慢,可将DrawStones()中的Ellipse()改为FillSolidRect()绘制实心圆(牺牲美观换速度),或引入CDC::SetStretchBltMode(COLORONCOLOR)优化缩放质量。

2.4 坐标映射:从像素点到19路棋盘坐标的精准转换

用户点击屏幕任意位置,程序必须将其转换为棋盘上的(row, col)坐标。这是所有棋类程序的基础能力,也是新手最容易写错的地方。CChessView中OnLButtonDown()处理流程如下:

  1. 调用GetClientRect(&rect)获取客户区矩形;
  2. 调用ScreenToClient(&point)将鼠标屏幕坐标转为客户区坐标;
  3. 计算棋盘左上角偏移量:int offsetX = (rect.Width() - BOARD_SIZE) / 2;(BOARD_SIZE=560,即19×19棋盘像素宽);
  4. 计算列号:int col = (point.x - offsetX + GRID_SPACING/2) / GRID_SPACING;(GRID_SPACING=30,即格距);
  5. 计算行号:int row = (point.y - offsetY + GRID_SPACING/2) / GRID_SPACING;。

注意:+ GRID_SPACING/2是四舍五入关键!否则(point.x - offsetX)为29时除以30得0,实际应归入第1列。原程序在CChessView::OnLButtonDown()第142行实现此逻辑,且做了边界保护:if (col < 0 || col > 18 || row < 0 || row > 18) return;。若你扩展为25路棋盘,只需修改BOARD_SIZE和GRID_SPACING宏定义,坐标映射逻辑完全复用。


3. 围棋规则引擎实现:气、提子、禁着点与打劫的C++编码细节

围棋规则看似简单,实则暗藏大量边界条件。这个VC++程序的规则引擎全部封装在CChessDoc中,未使用任何开源库,是学习游戏规则建模的优质范本。它不追求职业级AI,但每一步落子都经过严格规则校验,确保双人对战逻辑零错误。下面逐层拆解四个核心规则的C++实现要点。

3.1 气的计算:深度优先搜索(DFS)遍历连通块

“气”指棋子相邻空交叉点的数量。程序用CountLiberties(int x, int y)计算某颗棋子所在连通块的总气数。它不单独计算单颗子,而是以(x,y)为起点,DFS遍历所有同色相连棋子,收集它们所有相邻空点,去重后计数。关键点在于:必须用集合(std::set<CPoint>)存储气点,避免重复计数。例如一块由4颗黑子组成的直角形,中心空点会被4颗子同时计入,若不用集合去重,气数会严重高估。

int CChessDoc::CountLiberties(int x, int y) { if (m_board[x][y] == EMPTY) return 0; int player = m_board[x][y]; std::set<CPoint> liberties; std::vector<CPoint> stack; std::vector<bool> visited(19*19, false); stack.push_back(CPoint(x, y)); visited[x*19+y] = true; while (!stack.empty()) { CPoint cur = stack.back(); stack.pop_back(); // 检查上下左右四个方向 for (int i = 0; i < 4; i++) { int nx = cur.x + dx[i]; // dx = {0,0,1,-1} int ny = cur.y + dy[i]; // dy = {1,-1,0,0} if (nx < 0 || nx > 18 || ny < 0 || ny > 18) continue; if (m_board[nx][ny] == EMPTY) { liberties.insert(CPoint(nx, ny)); } else if (m_board[nx][ny] == player && !visited[nx*19+ny]) { visited[nx*19+ny] = true; stack.push_back(CPoint(nx, ny)); } } } return (int)liberties.size(); }

逻辑说明:visited数组防止DFS陷入死循环;dx/dy数组定义四邻域方向;liberties.insert()自动去重。此函数时间复杂度O(n),n为连通块大小,对19路棋盘完全可接受。

3.2 提子逻辑:气尽则提,需递归清理整块

当某方落子导致对方一块棋子气数为0时,必须提走整块。RemoveGroup(int x, int y)函数执行此操作。它与CountLiberties共享DFS遍历逻辑,但行为相反:遍历所有同色棋子,将m_board对应位置置为EMPTY,并累加提子数。关键陷阱在于:提子后可能使己方其他棋子获得新气,因此提子操作必须在规则校验最后一步执行。原程序在PlaceStone()中先调用CanPlaceStone()校验,仅当返回true才执行RemoveGroup(),避免无效提子。

void CChessDoc::RemoveGroup(int x, int y) { int player = m_board[x][y]; std::vector<CPoint> stack; stack.push_back(CPoint(x, y)); m_board[x][y] = EMPTY; m_nCaptured[player]++; // 记录提子数 while (!stack.empty()) { CPoint cur = stack.back(); stack.pop_back(); for (int i = 0; i < 4; i++) { int nx = cur.x + dx[i]; int ny = cur.y + dy[i]; if (nx < 0 || nx > 18 || ny < 0 || ny > 18) continue; if (m_board[nx][ny] == player) { m_board[nx][ny] = EMPTY; m_nCaptured[player]++; stack.push_back(CPoint(nx, ny)); } } } }

参数说明:m_nCaptured[1]存黑方提子数,m_nCaptured[2]存白方提子数,用于终局数子。注意m_nCaptured是CChessDoc成员变量,跨对局持久化——若需重置,应在CChessDoc::OnNewDocument()中清零。

3.3 禁着点判定:自杀步与全局同形的双重过滤

围棋禁止“自杀步”(落子后己方整块无气且未提对方子)。程序在CanPlaceStone()中分两步校验:
第一步,模拟落子:m_board[x][y] = player;
第二步,检查该位置所在连通块气数:if (CountLiberties(x, y) == 0),则进入第三步;
第三步,检查是否提走对方子:遍历四邻域,对每个EMPTY邻点调用CountLiberties(nx, ny),若结果为0则说明能提子,自杀步允许;否则禁止落子。

此逻辑覆盖了99%的禁着点,但遗漏了“打劫”(Ko)情形——即双方反复提一子形成循环。为此程序引入打劫检测。

3.4 打劫规则(Ko Rule):哈希快照与历史回溯

打劫是围棋最复杂的规则之一。程序采用“上一手提子位置记忆法”:CChessDoc维护CPoint m_koPoint,记录上一手提子的坐标。当玩家试图在m_koPoint落子时,必须满足“此步能提走对方子”,否则禁止。具体实现:

bool CChessDoc::IsKoMove(int x, int y, int player) { if (m_koPoint.x == -1) return false; // 无打劫状态 if (x != m_koPoint.x || y != m_koPoint.y) return false; // 检查在此处落子能否提子 m_board[x][y] = player; bool canCapture = false; for (int i = 0; i < 4; i++) { int nx = x + dx[i], ny = y + dy[i]; if (nx < 0 || nx > 18 || ny < 0 || ny > 18) continue; if (m_board[nx][ny] == 3-player && CountLiberties(nx, ny) == 0) { canCapture = true; break; } } m_board[x][y] = EMPTY; // 恢复 return !canCapture; }

注意:m_koPoint在每次成功提子后更新为被提子的坐标(即RemoveGroup()前记录),并在PlaceStone()结尾重置为CPoint(-1,-1)。此实现虽非职业级(未检测长生、循环劫),但完全满足双人对战需求,且代码清晰易懂。


4. 文件操作与SGF支持:.sgf对局文件的读写与解析避坑指南

这个VC++围棋程序支持标准SGF(Smart Game Format)文件的读写,让你能保存对局、复盘分析、甚至导入职业棋谱。SGF是纯文本格式,以分号开头,括号嵌套属性,如;B[dd]表示黑方在d4落子。但直接手写解析器极易翻车——原程序采用轻量级状态机解析,避开正则表达式和递归下降的复杂性,是学习文件格式解析的极佳案例。

4.1 SGF写入:按顺序序列化每一步

CChessDoc::SaveSGF(CFile& file)函数将当前对局写入SGF文件。它不生成完整SGF头(如GM[1]FF[4]CA[UTF-8]),而是聚焦核心:(;FF[4]CA[UTF-8]AP[VCChess:1.0]SZ[19]KM[6.5]PB[Black]PW[White]RE[W+R]+ 所有B[xx]/W[xx]节点 +)。关键点在于坐标转换:SGF用小写字母a-s表示1-19路,'a'+col得到列字符,'a'+(18-row)得到行字符(因SGF坐标系原点在左上,而程序数组原点在左上,但SGF行号从上到下递增,程序row从上到下递增,故需18-row)。

void CChessDoc::SaveSGF(CFile& file) { CString str; str.Format(_T("(;FF[4]CA[UTF-8]AP[VCChess:1.0]SZ[19]KM[6.5]PB[Black]PW[White]")); file.Write(str, str.GetLength() * sizeof(TCHAR)); for (int i = 0; i < m_nMoveCount; i++) { int x = m_moves[i].x, y = m_moves[i].y; TCHAR col = _T('a') + y; // 列:y=0→'a', y=18→'s' TCHAR row = _T('a') + (18 - x); // 行:x=0→'s', x=18→'a' str.Format(_T("%c[%c%c]"), m_moves[i].player == 1 ? _T('B') : _T('W'), col, row); file.Write(str, str.GetLength() * sizeof(TCHAR)); } file.Write(_T(")"), 2 * sizeof(TCHAR)); }

逻辑说明:m_moves[]是CChessDoc中存储的历史步数数组,m_nMoveCount为总步数。此写入逻辑简洁可靠,但注意:若对局未结束,RE[]属性应为空,原程序未动态写入胜负结果,需手动补全。

4.2 SGF读取:状态机跳过注释与嵌套括号

CChessDoc::LoadSGF(CFile& file)是真正体现工程功力的部分。SGF文件可能包含多层嵌套括号、注释C[xxx]、变化分支AB[aa]等。原程序不追求100%兼容,而是用状态机安全跳过无关内容:

  • 状态0:等待(,跳过所有空白和;;
  • 状态1:读取属性名(如B、W、C),遇到[进入状态2;
  • 状态2:读取属性值,遇到]返回状态1,遇到(递归进入子节点(但本程序忽略子节点);
  • 遇到C[注释、AB[放置子等非核心属性,直接跳过至]。
bool CChessDoc::LoadSGF(CFile& file) { char buf[1024]; int state = 0; // 0: wait '(', 1: read prop, 2: read value CString prop; while (file.Read(buf, 1) == 1) { char c = buf[0]; if (state == 0) { if (c == '(') state = 1; continue; } if (state == 1) { if (c == '[') { state = 2; prop.Empty(); continue; } if (c == ')' || c == ';') continue; prop += c; } if (state == 2) { if (c == ']') { if (prop == "B" || prop == "W") { // 解析坐标:下一个字符是列,再下一个是行 file.Read(buf, 2); int col = buf[0] - 'a'; int row = 18 - (buf[1] - 'a'); // SGF行号倒置 if (col >= 0 && col <= 18 && row >= 0 && row <= 18) { PlaceStone(row, col, prop == "B" ? 1 : 2); } } state = 1; } } } return true; }

参数说明:file.Read(buf, 2)连续读取两个字符作为坐标,buf[0]是列字母,buf[1]是行字母。18 - (buf[1] - 'a')将SGF的s(第19行)转为程序数组索引18,a(第1行)转为0。此逻辑正确处理了SGF坐标系与程序数组的映射。

4.3 常见问题与避坑:SGF解析的五个血泪经验

现象 → 原因 → 解决

  1. SGF文件打开后棋盘全空,但文件确认有B[dd]等记录
    → 原因:SGF文件含BOM头(UTF-8 with BOM),CFile::Read()读取首3字节0xEF,0xBB,0xBF后,后续字符偏移错乱
    → 解决:在LoadSGF()开头添加BOM跳过逻辑:char bom[3]; if (file.Read(bom,3)==3 && bom[0]==0xEF && bom[1]==0xBB && bom[2]==0xBF) {} else file.Seek(-3,CFile::begin);

  2. 导入职业棋谱时,程序崩溃在PlaceStone()的CountLiberties()中
    → 原因:职业棋谱含AB[aa](预先放置黑子)、AW[bb](预先放置白子)等初始化指令,原程序未解析,导致m_board未初始化就调用规则函数
    → 解决:在状态机中增加对AB/AW的处理,解析后直接设置m_board[x][y]=1/2,不触发规则校验

  3. 保存的SGF文件用专业软件(如Sabaki)打开报错“invalid property”
    → 原因:原程序未写入必需的GM[1](围棋类型)和FF[4](SGF版本)头部,Sabaki严格校验
    → 解决:在SaveSGF()开头str.Format()中补全:_T("(;GM[1]FF[4]CA[UTF-8]AP[VCChess:1.0]SZ[19]KM[6.5]PB[Black]PW[White]")

  4. 读取含中文注释C[黑方妙手]的SGF时,程序卡死
    → 原因:状态机在state==2时,遇到中文字符(UTF-8多字节)的中间字节,误判为]
    → 解决:放弃单字节读取,改用file.ReadString(str, 1024)读整行,再用_tcstok_s()分割,或限定只解析ASCII属性

  5. 同一SGF文件,用VS2010编译版可读,VS2019编译版读取失败
    → 原因:VS2019默认CFile使用Unicode模式,而原程序假设ANSI,file.Read(buf,1)读取的是宽字符首字节
    → 解决:统一使用CStdioFile替代CFile,或在LoadSGF()中强制file.SetLength(file.GetLength());重置读取位置


5. 编译与调试实战:VS2010/2015/2019兼容性适配与GDI性能优化技巧

拿到源码后,第一步永远是编译通过。这个VC++围棋程序原始基于VS2010开发,但在VS2015/2019中编译会遇到三类典型问题:MFC库路径变更、GDI绘图API弃用警告、高DPI缩放异常。下面给出可直接粘贴的解决方案,每一步都经实测验证。

5.1 VS2015+编译修复:MFC路径与字符集配置

VS2015起,MFC库默认安装路径变更,且项目属性中“字符集”默认为“使用Unicode字符集”。原程序使用CString和_T()宏,兼容Unicode,但需手动修正两处:
第一,在项目属性 → 常规 → 使用MFC中,必须选择“在共享DLL中使用MFC”(而非静态链接),否则链接时报LNK2005: _DllMain@12 already defined;
第二,在项目属性 → 高级 → 字符集中,确认为“使用Unicode字符集”,并检查stdafx.h中是否定义_UNICODE和UNICODE(原程序已定义,无需修改);
第三,最关键的修复:CChessView::OnDraw()中TextOut()调用需显式转换字符串。原代码pDC->TextOut(10,10,"Black");在Unicode下编译失败,应改为pDC->TextOut(10,10,_T("Black"));或pDC->TextOut(10,10,_T("Black"), 5);(5为字符串长度)。

提示:若你看到error C2664: 'int CDC::TextOutW(int,int,LPCWSTR,int)',说明编译器认为你传入ANSI字符串。统一使用_T()宏是最稳妥方案。

5.2 GDI双缓冲性能瓶颈定位与优化

在1920×1080屏幕上运行时,OnDraw()可能占CPU 15%以上。性能分析显示,DrawStones()中CDC::Ellipse()绘制棋子是主要开销。优化方案有二:
方案一(推荐):用CDC::FillSolidRect()绘制实心圆代替Ellipse()。修改DrawStones()中绘图部分:

// 原代码(慢) pDC->Ellipse(x-8, y-8, x+8, y+8); // 替换为(快3倍) CRect stoneRect(x-8, y-8, x+8, y+8); pDC->FillSolidRect(&stoneRect, player == 1 ? RGB(0,0,0) : RGB(255,255,255));

方案二(进阶):预生成棋子位图。在CChessView::OnInitialUpdate()中创建两个CBitmap(黑白子),OnDraw()中用CDC::DrawState()绘制,彻底消除实时绘图开销。但需管理位图生命周期,适合追求极致性能的场景。

5.3 高DPI适配:解决棋盘变形与坐标偏移

在4K屏(缩放150%)下,原程序棋盘严重变形,鼠标点击坐标错位。根本原因是MFC未启用DPI感知。修复步骤:

  1. 在CChessApp::InitInstance()开头添加:AfxEnablePerUserTweaks(FALSE);;
  2. 在项目属性 → 清单工具 → 输入和输出 → 启用DPI感知中设为“Yes”;
  3. 关键一步:重写CChessView::OnSize(),根据DPI缩放BOARD_SIZE和GRID_SPACING:
void CChessView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); // 获取DPI缩放因子 HDC hdc = ::GetDC(m_hWnd); int dpiX = ::GetDeviceCaps(hdc, LOGPIXELSX); ::ReleaseDC(m_hWnd, hdc); float scale = dpiX / 96.0f; // 96为标准DPI BOARD_SIZE = (int)(560 * scale); GRID_SPACING = (int)(30 * scale); }

注意:BOARD_SIZE和GRID_SPACING需声明为static或CChessView成员变量,原程序定义为全局宏,必须重构。此修改确保棋盘像素尺寸随DPI线性缩放,坐标映射逻辑保持不变。

5.4 调试技巧:快速定位落子失败的三类断点

当用户点击棋盘无反应,不要盲目看代码,按此顺序设断点:

  1. 第一断点:CChessView::OnLButtonDown()入口,确认是否被调用(若未触发,检查CMainFrame是否禁用了WS_CHILD样式);
  2. 第二断点:CChessDoc::CanPlaceStone()返回值,若返回false,查看CountLiberties()中DFS遍历是否越界(常见于nx<0未检查);
  3. 第三断点:CChessView::OnDraw()末尾,BitBlt()后立即调用InvalidateRect(NULL)强制重绘,排除GDI缓存未刷新问题。

我一般会在CanPlaceStone()开头加日志:TRACE(_T("Check move at %d,%d for player %d\n"), x, y, player);,配合Output窗口实时观察。从那以后我每次调试图形界面交互,都强制走一遍这三类断点——省下至少两小时抓包和瞎猜的时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 2:06:00

SINUMERIK RPC 计算机链路:双向通信机制与上位机联调避坑指南

简介&#xff1a;这份文档面向从事西门子数控系统二次开发与上位机通信的工程师&#xff0c;聚焦SINUMERIK 840D/840Di/810D平台上的RPC远程过程调用机制&#xff0c;帮助读者理清主机与控制系统之间的接口规范与调用方式。内容涵盖上位机接口、PLC/NCK接口、HMI与NCK/PLC的OEM…

作者头像 李华
网站建设 2026/11/2 2:04:54

C# WinForm企业号消息推送Demo拆解与避坑

简介&#xff1a;面向需要对接微信企业号的C#开发者&#xff0c;这份示例工程围绕主动推送消息展开&#xff0c;覆盖文本、图片、语音、视频与图文等消息类型的构建与发送&#xff0c;并涉及AppID/AppSecret配置、素材上传获取媒体ID、SendMessage调用及事件监听等常见环节&…

作者头像 李华
网站建设 2026/11/2 2:03:26

VSCode配置C/C++环境:三份JSON与CMake调试避坑指南

简介&#xff1a;围绕 VSCode 编辑器入门与 C/C 开发环境搭建整理的图文教程资料包&#xff0c;面向刚接触 VSCode 的学生、自学者以及需要从其他 IDE 迁移的开发者&#xff0c;重点解决插件选择、编译调试配置与任务终端联动等常见卡点。压缩包共 1132 个文件&#xff0c;约 2…

作者头像 李华
网站建设 2026/11/2 2:02:59

操作系统核心概念图解:并发、虚拟、同步与存储管理

简介&#xff1a;这是一份面向计算机专业本科生的操作系统课程期末复习笔记&#xff0c;聚焦核心概念梳理与考点精讲&#xff0c;帮助学生高效应对期末考试。笔记系统覆盖操作系统目标&#xff08;方便性、有效性、可扩充性、开放性&#xff09;、发展历程&#xff08;人工操作…

作者头像 李华
网站建设 2026/11/2 2:02:58

操作系统核心概念拆解:并发、内存管理与I/O原理实战

简介&#xff1a;本资源是一份面向计算机专业本科生的操作系统课程期末复习笔记&#xff0c;聚焦核心概念梳理与考点精讲&#xff0c;帮助学生高效应对期末考试。笔记系统覆盖操作系统目标&#xff08;方便性、有效性、可扩充性、开放性&#xff09;、发展历程&#xff08;人工…

作者头像 李华
网站建设 2026/11/2 1:59:53

【QT C++】常用设计模式

前言 本篇的设计模式主要是&#xff1a;单例模式、工厂方法模式、策略模式、观察者模式、适配器模式。 单例模式 保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 特点 唯一实例&#xff1a;类自己控制实例创建&#xff0c;保证全局只有一个对象。私有构…

作者头像 李华