尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Qt可视化21点游戏课设:逻辑分层与状态机设计

Qt可视化21点游戏课设:逻辑分层与状态机设计 简介基于Qt 5.12开发的可视化21点纸牌游戏是一份面向C初学者的GUI课程设计源码包。项目以按钮交互模拟玩家与电脑对弈完整实现要牌、停牌、加倍、分牌等规则逻辑并处理好点数累计与超过21点判负的机制。资源共86个文件其中58张扑克牌PNG素材覆盖完整牌面10个C源文件与8个头文件构成核心程序另含2个UI界面设计文件、项目配置文件和图标资源压缩包仅2.2MB目录按场景、模型等模块划分便于逐段阅读。目前已有1469人下载学习。通过阅读源码可以系统掌握Qt窗口与布局管理网格布局、盒式布局、信号与槽事件处理、QResource资源加载、随机发牌算法和异常兼容处理等实践技能。对于课设答辩或入门Qt开发这套代码提供了可直接运行的完整示例也可在其上扩展动画效果或联网对战适合作为二次开发的起点。1. Qt可视化界面版21点游戏课设真正的难点不在画窗口拿到“Qt可视化界面版21点游戏课设”这个题大多数人的第一反应是去找扑克牌图片素材或者研究怎么把窗口做得花哨。做过一遍之后你会发现界面只占三成工作量真正让人熬夜的是牌局规则怎么用代码表达清楚A到底是1还是11庄家到17停牌这个边界条件藏在哪里黑杰克什么时候该按1.5倍赔付。这些规则如果直接写死在按钮点击函数里界面一调整逻辑就跟着散架这是Qt课设最常见的翻车点。这篇文章面向正在做Qt课程设计的学生也面向想拿游戏项目练手信号槽、布局和状态机的Qt初学者目标是让你用最少的资源做出一个能跑、能答辩、能讲清楚内部结构的21点游戏。2. 从牌堆到牌力用C把21点规则先写成能测的逻辑层2.1 用结构体还是QString表示一张牌数据结构的取舍课程设计里最常见的做法有两种一种是用QString直接存“红桃A”“黑桃10”另一种是定义一个Card结构体把花色和点数分开存。我强烈建议用结构体因为21点计算牌力时只关心点数显示时又需要花色和名称这两个需求在数据结构上应该分开而不是存一个拼好的字符串再去反复解析。// card.h #pragma once #include QString enum Suit { Spade, Heart, Club, Diamond }; struct Card { Suit suit; int rank; // 1A, 2~10按面值, 11J, 12Q, 13K int value() const { if (rank 10) return 10; // J/Q/K 都按10点 return rank; } QString text() const { static const char* rankStr[] {, A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K}; static const char* suitStr[] {♠, ♥, ♣, ♦}; return QString(%1%2).arg(suitStr[suit]).arg(rankStr[rank]); } };代码里的rank字段是牌力计算的核心依据1代表A11到13分别代表JQK。value()函数把JQK统一折算成10点这个函数在后续所有牌力计算里都会复用避免在界面层到处写if判断。text()函数只在显示时调用它把花色符号和点数拼成一个短字符串可以直接放到QLabel上。两个函数职责分离后面界面做不做图片升级都不影响逻辑。这里有一个容易忽略的参数细节花色枚举没有给具体数值默认从0到3这正好对应suitStr数组的下标。如果你在别处把花色存成QString后续排序、判同花这类操作都会更啰嗦虽然21点不需要同花判断但这种设计习惯能帮你少走弯路。2.2 牌堆类洗牌、发牌和越界保护牌堆用标准库容器存储52张牌洗牌用std::shuffle而不是已经废弃的std::random_shuffle。Qt 5.15对应的编译器大多支持C17random_shuffle在C17里已经被移除继续用它你会得到编译错误。// deck.h #pragma once #include vector #include algorithm #include random #include card.h class Deck { public: Deck() { rebuild(); } void rebuild() { cards.clear(); for (int s 0; s 4; s) for (int r 1; r 13; r) cards.push_back({static_castSuit(s), r}); } void shuffle() { std::mt19937 rng(std::random_device{}()); std::shuffle(cards.begin(), cards.end(), rng); } Card deal() { if (cards.empty()) rebuild(); // 牌发完自动重开一副 Card c cards.back(); cards.pop_back(); return c; } private: std::vectorCard cards; };deal函数里加了一个保护如果牌堆空了就自动rebuild。21点一局最多用掉十几张牌52张通常绰绰有余但课设演示时如果连续玩很多局或者你加了多副牌功能这个保护能让程序不因越界崩溃。std::random_device用来给mt19937提供随机种子每次启动游戏洗牌结果都不同这一点比用固定种子更能通过老师的演示检查。2.3 手牌点数计算A的1/11换算用减差值而不是重新遍历21点规则里最绕的就是A既可以当1点也可以当11点计算时要先把所有A按1点累加再判断有没有空间把某个A升级成11点。很多课设代码在拿到一手牌后遍历两次第一次数A的个数第二次算总值结果状态一变就出bug。我的做法是一次遍历同时算总值和A的数量最后统一调整。// handvalue.h #pragma once #include vector #include card.h // 返回手牌最优点数soft表示当前手牌存在“A算11点不爆”的软牌状态 int handValue(const std::vectorCard hand, bool soft) { int total 0; int aces 0; soft false; for (const Card c : hand) { total c.value(); if (c.rank 1) aces; } while (total 11 aces 0) { total 10; // 把一张A从1点升级为11点 --aces; soft true; if (total 21) break; // 升级后超过21就退回进入while条件判断 } if (aces 0 total 11) soft true; return total; }这里要注意循环条件里的细节total 11时才考虑升级因为只有总值不超过11加10才不爆。如果手牌是A9total先是10升级成20soft为true这就是软20因为如果后续再要一张超过1点的牌还可以把A降回1点不爆。如果手牌是A57total是13不具备升级条件A只能算1点。很多翻车案例就是没考虑这个“软牌降级”的场景只算了一次就返回。软牌状态在界面层要不要展示取决于你的课设要求但逻辑层必须把这个信息返回。事实上庄家17点停牌的判断也依赖soft庄家拿到软17时有的规则要求继续要牌有的规则要求停牌逻辑层统一处理会比界面层写if容易测试得多。2.4 庄家自动打法规则与界面分离的边界庄家要牌到17点这个规则应该放在一个独立的Game类或者至少是独立函数里。课程设计常见的演示路径是“玩家点完停牌后庄家自动要牌”如果你把庄家的while循环直接写在按钮槽函数里界面会在循环期间完全卡死因为UI线程被阻塞了。正确的做法是把庄家每一步要牌封装成一个函数由界面层的定时器一步步触发。// gamelogic.h #pragma once #include vector #include card.h enum GameResult { PlayerBust, DealerBust, PlayerWin, DealerWin, Push }; // 根据双方点数判定结果不关心牌型只关心数值 GameResult judgeRound(int playerTotal, int dealerTotal, bool playerBust, bool dealerBust);judgeRound的判定顺序是先看双方是否爆牌爆牌优先级最高然后比大小平局返回Push。这个函数写完后可以直接跑单元测试不需要打开界面。逻辑层和界面层的边界就划在这里逻辑层只处理数值和规则界面层只负责显示和收集操作。实际开发中我习惯把Deck、handValue、judgeRound这些代码放在一个logic文件夹里界面文件放在ui文件夹里这样即使后面想从QWidget换成QML逻辑层一行都不用动。这正是Qt课程设计答辩时最值得讲的亮点你的代码分层是清晰的。3. Qt界面搭建用Qt Designer辅助布局与卡片显示方案3.1 主窗口选QWidget还是QMainWindow课设推荐的窗口骨架如果你在Qt Creator里新建项目向导会让你选QMainWindow、QWidget还是QDialog。很多课设模板默认选QMainWindow因为它自带菜单栏、状态栏和工具栏。但一个单局21点根本用不到菜单栏QMainWindow额外的布局层级反而让新手犯迷糊。我一般选QWidget做根窗口所有控件放在垂直布局里结构一目了然。// mainwindow.h #pragma once #include QWidget class QLabel; class QPushButton; class MainWindow : public QWidget { Q_OBJECT public: explicit MainWindow(QWidget* parent nullptr); private slots: void onStartClicked(); void onHitClicked(); void onStandClicked(); private: void setupUi(); QLabel* dealerCardsLabel; QLabel* playerCardsLabel; QLabel* resultLabel; QPushButton* startButton; QPushButton* hitButton; QPushButton* standButton; };QWidget默认没有菜单栏你能放的东西就是中心控件本身布局代码更好写。setupUi函数负责创建所有子控件并放到布局里构造函数里调用它。这种写法把界面构造和逻辑处理分开后期把一个按钮从布局里拿出来调整位置不会误删逻辑代码。3.2 展示手牌的三种方案对比QLabel换图、样式表、自绘展示一张牌有三种常见方案准备54张扑克牌图片用QLabelQPixmap显示用样式表把QLabel画成卡片样式或者继承QWidget用paintEvent自绘。表格对比如下方案视觉上限开发量资源依赖推荐场景QLabel图片高小需要图片文件有现成素材时QLabelQSS样式表中小无课设最稳妥paintEvent自绘高大无想展示绘图能力时课设时间紧张的时候我推荐第二种。不用找图片素材不用担心图片失真一套样式表就能把牌面画出来。每张牌的牌背就用深色背景加边框来实现。// mainwindow.cpp 片段 void MainWindow::setupUi() { dealerCardsLabel new QLabel(庄家牌, this); playerCardsLabel new QLabel(玩家牌, this); resultLabel new QLabel(点击“开始”发牌, this); dealerCardsLabel-setMinimumHeight(120); playerCardsLabel-setMinimumHeight(120); dealerCardsLabel-setStyleSheet( QLabel { background-color: #2d5f3e; color: white; border-radius: 8px; padding: 8px; font-size: 24px; }); playerCardsLabel-setStyleSheet( QLabel { background-color: #f5f5dc; color: #222; border-radius: 8px; padding: 8px; font-size: 24px; }); startButton new QPushButton(开始, this); hitButton new QPushButton(要牌, this); standButton new QPushButton(停牌, this); hitButton-setEnabled(false); standButton-setEnabled(false); }样式表里的background-color模拟牌桌底色绿色给庄家区域、米色给玩家区域这样即使没有图片课程设计截图看起来也像回事。font-size设到24px是为了让“A♠”这类文本在120像素高的区域里足够清晰。setMinimumHeight保证窗口拉伸时标签高度不会塌掉这是界面布局里容易被忽略的细节。3.3 布局结构不用setGeometry用QHBoxLayout和QVBoxLayout新手写Qt界面最顺手的就是setGeometry硬坐标代码写起来快但窗口一拉伸控件就乱飞。课程设计展示时老师很容易拖拽窗口大小一旦乱布局分数就不好看了。正确做法是把所有控件交给布局管理器。void MainWindow::setupUi() { // 上半区庄家 QVBoxLayout* dealerLayout new QVBoxLayout; dealerLayout-addWidget(dealerCardsLabel); // 中间区结果提示 QVBoxLayout* centerLayout new QVBoxLayout; centerLayout-addWidget(resultLabel); // 下半区玩家 QVBoxLayout* playerLayout new QVBoxLayout; playerLayout-addWidget(playerCardsLabel); // 按钮区 QHBoxLayout* buttonLayout new QHBoxLayout; buttonLayout-addWidget(startButton); buttonLayout-addWidget(hitButton); buttonLayout-addWidget(standButton); buttonLayout-addStretch(); // 总布局 QVBoxLayout* mainLayout new QVBoxLayout(this); mainLayout-addLayout(dealerLayout, 1); mainLayout-addLayout(centerLayout, 0); mainLayout-addLayout(playerLayout, 1); mainLayout-addLayout(buttonLayout, 0); }addLayout的第二个参数是拉伸因子庄家区域和玩家区域各占1份中间结果提示不参与拉伸按钮区也不拉伸。这样窗口变大时多出来的空间会分配给两个牌区提示文字和按钮始终居中在合适位置。这里没有用QSpacerItem因为拉伸因子已经够了多用反而让代码难读。另外有一点值得说布局的嵌套顺序决定了垂直方向的结构先建三个子布局再合并成mainLayout比在一个布局里塞五个控件更容易调整。如果想在两侧加留白可以在mainLayout两侧再加QHBoxLayout包一层而不是到处setContentsMargins。这个习惯从课设带出去工作后写复杂表单也够用。3.4 桌面布局和Qt Designer的关系热词里经常看到qt designer界面设计Qt Creator自带的Qt Designer可以把.ui文件拖拽设计再通过uic生成界面代码。课设里用.ui文件和纯代码布局都可行但我个人更推荐纯代码代码里的注释、变量名、布局结构都能直接展示答辩被问到“这个按钮在哪个文件里”时你一句“在mainwindow.cpp的setupUi里”比“在designer里拖的”更有说服力。如果你非要用Qt Designer注意一个坑.ui文件里的对象名和代码里手工创建的变量名冲突时编译阶段很难察觉运行时可能出现改了Designer界面但代码不生效的诡异问题。解决方案是二选一要么全走.uiui_xxx.h要么全走代码别混着来。4. 用信号槽把按钮和牌局状态机接起来4.1 四个游戏状态Idle、PlayerTurn、DealerTurn、RoundEnd21点一局牌的流程是固定的发牌后进入玩家回合玩家停止要牌后进入庄家回合双方结算后回到空闲等待下一局。这个流程用枚举状态机来管理比在按钮槽函数里写一堆if else清晰得多也更接近正式项目的写法。// gamestate.h #pragma once enum class GameState { Idle, // 空闲等待开始 PlayerTurn, // 玩家回合可要牌或停牌 DealerTurn, // 庄家回合自动要牌 RoundEnd // 结算完成等待下一局 };状态机的核心原则是任何一个按钮响应函数先检查当前状态是否允许这个操作不允许就直接return。比如要牌按钮只在PlayerTurn状态下有效如果玩家已经爆牌或者已经停牌点了就不该有任何反应。这个检查放在界面层还是逻辑层都可以我习惯放在按钮槽函数最开头因为这样代码读起来最直接先看状态再走流程。4.2 发牌、要牌、停牌三个按钮的槽函数实现信号槽是Qt课设的必考知识点把clicked信号和自定义槽函数用new语法连接已经是Qt 5之后的标准写法。下面这段代码连接了三个按钮每个槽函数内部都按状态机模型处理。// mainwindow.cpp void MainWindow::setupUi() { // ... 前面的控件创建代码 ... connect(startButton, QPushButton::clicked, this, MainWindow::onStartClicked); connect(hitButton, QPushButton::clicked, this, MainWindow::onHitClicked); connect(standButton, QPushButton::clicked, this, MainWindow::onStandClicked); } void MainWindow::onStartClicked() { if (state ! GameState::Idle) return; deck.shuffle(); playerHand.clear(); dealerHand.clear(); playerHand.push_back(deck.deal()); dealerHand.push_back(deck.deal()); playerHand.push_back(deck.deal()); dealerHand.push_back(deck.deal()); state GameState::PlayerTurn; startButton-setEnabled(false); hitButton-setEnabled(true); standButton-setEnabled(true); refreshDisplay(); }onStartClicked里先重置手牌数组再交替各发两张牌然后切换状态并更新按钮可用性。注意发牌顺序玩家第一张、庄家第一张、玩家第二张、庄家第二张和真实赌桌规则一致。逻辑层的dealerHand使用std::vector 这里不需要卡片控件对象因为显示用的QLabel文本是从这个数组生成的。refreshDisplay函数做的事情是遍历两个手牌数组把所有卡的text()拼成一个字符串同时计算双方点数显示到结果标签上。这里不再单独列代码因为重复逻辑简单但你要记住刷新显示必须在每次发牌、要牌、停牌之后调用忘记调用就会发生界面显示和实际状态不一致的诡异问题这是课设里最常见的“黑匣子”现象。4.3 要牌与停牌的边界条件爆牌、21点、庄家回合要牌不止是“往数组里塞一张卡再刷新”还伴随着爆牌检查。玩家要牌后点数超过21就直接进入RoundEnd不能继续操作恰好21点时应该在界面提示“21点”并自动进入庄家回合。这两个边界条件漏掉任何一个演示时都会被老师一眼看穿。void MainWindow::onHitClicked() { if (state ! GameState::PlayerTurn) return; playerHand.push_back(deck.deal()); bool soft false; int total handValue(playerHand, soft); refreshDisplay(); if (total 21) { resultLabel-setText(爆牌你输了); state GameState::RoundEnd; hitButton-setEnabled(false); standButton-setEnabled(false); startButton-setEnabled(true); } else if (total 21) { onStandClicked(); // 21点自动停牌 } } void MainWindow::onStandClicked() { if (state ! GameState::PlayerTurn) return; state GameState::DealerTurn; hitButton-setEnabled(false); standButton-setEnabled(false); dealerTurn(); // 进入庄家流程 }这里有个容易踩坑的点onStandClicked里直接调用另一个槽函数这在Qt里是合法的、也鼓励的因为槽函数就是普通成员函数。但要注意别在onStandClicked里又connect同一个信号否则会出现重复信号连接的bug。巧妙复用槽函数可以简化状态流转玩家21点时不需要额外写一段庄家回合代码直接复用停牌逻辑即可。onHitClicked里的total21判断放在total21之后如果爆了就不执行停牌。这个顺序看起来是小事但如果你把两个判断写反手牌21点但已经爆的时候就会误判成自动停牌。这种边界问题很难靠跑一次发现最好的办法是在代码里直接按“先判断爆牌、再判断精确21点”的顺序写清楚。4.4 庄家回合的异步处理QTimer::singleShot避免界面卡死庄家要牌到17点这个逻辑如果写成while循环程序会一直等循环结束才刷新界面视觉效果就是点完“停牌”后界面卡住一阵子。正确做法是用QTimer::singleShot让庄家每一步延迟几百毫秒执行形成逐张翻牌的效果。void MainWindow::dealerTurn() { if (state ! GameState::DealerTurn) return; bool soft false; int dealerTotal handValue(dealerHand, soft); if (dealerTotal 17) { dealerHand.push_back(deck.deal()); refreshDisplay(); QTimer::singleShot(600, this, MainWindow::dealerTurn); } else { finishRound(); } }QTimer::singleShot的第一个参数是毫秒延迟第二个参数是接收对象第三个是槽函数。dealerTurn里判断庄家点数不足17就再要一张牌600毫秒后再次进入dealerTurn达到17或以上就调用finishRound结算。这种递归调用不会阻塞UI线程玩家能看到庄家一张张翻牌的过程演示效果也好。finishRound里要做的事包括计算双方点数并调用judgeRound判定胜负更新结果标签把所有按钮恢复成可用状态同时把state置回Idle。注意状态切换必须在按钮可用性更新之前或之后保持一致不要出现“按钮已经可点但state还是DealerTurn”的窗口期否则玩家在结算瞬间点“开始”会触发异常行为。4.5 按钮可用性驱动的用户引导状态机的另一个作用是控制按钮的可用性让玩家不被告知也明白当前能做什么。开局只能点“开始”发完牌后只有“要牌”和“停牌”可用“开始”灰掉结算后“开始”重新亮起其他两个灰掉。这些setEnabled调用集中在状态切换处而不是散落在各处。这种做法有一个好处演示时状态条理清楚老师问“这个按钮什么时候变灰”你直接答“状态机从PlayerTurn切到DealerTurn时”。如果需求变更比如加了“加倍”按钮只需要在状态机里加一个分支不需要改动其他按钮的逻辑。5. Qt课设常见坑从编译失败到运行期边界问题5.1 现象点击“要牌”按钮后程序闪退报“0000005”访问冲突原因手牌容器未初始化。常见于直接定义std::vector playerHand成员变量但在某个槽函数里没有clear就push_back或者deck为空时调用了deal。Qt的QVector和std::vector在debug模式下会提供越界检查但release模式下直接踩内存闪退点位千奇百怪。解决在每个回合开始前显式调用playerHand.clear()和dealerHand.clear()deck.deal()内部做空牌堆保护。如果你用了QVector建议统一用at()而不是[]访问元素at()带越界检查[]不带。这一点在Qt 5.15里依然成立属于C容器通用注意事项。5.2 现象A的点数总是算错手牌A10显示21但结果却是黑杰克判到了庄家头上原因黑杰克的定义是“开局两张牌合计21点”而“要牌要出来的21点”只能算普通21点不算黑杰克赔付率不同。很多课设只写了judgeRound没有在开局发完牌后单独判断黑杰克结果就是把普通21点当黑杰克处理了。解决在onStartClicked发完牌之后加一个判断如果玩家两张牌点数之和等于21直接标记blackJacktrue结算时按照1.5倍赔付计算。这里必须用handValue函数计算而不是直接比较两张牌的点数和因为AJ这种组合只有通过value()计算才能正确得到21点。另外注意牌数判断玩家手牌size必须等于2才算黑杰克要牌要到6张牌的21点不算。5.3 现象Qt Creator编译报“cannot mix incompatible Qt library (version ex50601) with this library”或“unknown module(s) in qt: webenginewidgets”原因前者是编译时链接的Qt库和运行时加载的Qt库版本不一致常见于电脑里装了多个Qt版本或者环境变量PATH指向了旧版本。后者是.pro文件里加了QT webenginewidgets但当前安装的Qt套件里没有这个模块。热词里的qt安装问题、qt 5.15.2下载大多属于这一类。解决qmake项目打开.pro文件检查QT变量21点游戏只需要QT core gui和针对高版本Qt的widgets。然后确认Tools选项里选中的套装和编译器匹配MinGW版就配MinGW编译器MSVC版就配MSVC编译器。清理构建目录、重新qmake、再构建多数版本冲突都能解决。如果你根本不用WebEngine千万不要为了贪图齐全乱加模块这个模块体积大且编译慢课设完全用不到。5.4 现象窗口最大化后卡片或者文字控件挤成一团或者出现大片空白原因使用了绝对坐标setGeometry定位控件没有放进布局管理器或者布局里设置了固定大小setFixedSize窗口拉伸时控件尺寸不变。解决把所有控件加入布局管理器并去掉不必要的setFixedSize。QLabel显示牌文本时用setMinimumHeight保证标签在窗口缩小时也能展示内容。如果确实需要固定尺寸的卡片图片把固定大小的QLabel放进QHBoxLayout并设置居中对齐让布局管理器决定卡片在整个窗口中的位置而不是自己死磕坐标。5.5 现象界面完全正常但发布到别的机器上运行时提示缺少Qt5Core.dll等动态库原因开发机上Qt库的路径没有随exe一起打包。Qt Creator的debug模式运行会自动找开发目录里的库但生成的exe拷到其他机器就找不到动态库。这是课设演示时的经典翻车现场。解决发布时用Qt自带的windeployqt工具命令行进入exe所在目录执行windeployqt 你的程序名.exe它会自动拷贝所需的Qt模块和DLL。如果用了MinGW套装还需要拷贝libgcc_s_seh-1.dll等运行库这时用windeployqt --compiler-runtime参数一并处理。发布之前把exe放到一台没有装Qt的虚拟机里跑一遍再提交这是最可靠的验证方式。6. 答辩前的最后升级加倍、验证和演示顺序6.1 加一个“加倍”按钮状态机扩展的实战练习如果课设要求功能更丰富最值得加的是加倍Double Down玩家在拿到前两张牌后可以选择加倍同时筹码翻倍、且只能再要一张牌。实现方式是在GameState里加一个DoubleDown状态或者在PlayerTurn状态里加一个bool doubleDown标志。按钮槽函数里先检查手牌数量等于2然后设置加倍标志并立刻发一张牌发完后跳过玩家操作直接进入庄家回合。这个改动正好能向老师展示你对状态机模型的理解深度。6.2 用几组固定牌型验证逻辑不要只靠随机牌局答辩前用构造好的手牌去验证核心规则比玩十局全靠运气可靠得多。我常用的验证方式是在一个debug槽函数里直接给playerHand和dealerHand赋值固定牌型而不是走洗牌发牌流程。测试用例包括A920的软牌、A5713的A降级、10JK爆牌、两张牌黑杰克、庄家16要牌17停牌边界。把这些用例的结果打印出来确认逻辑无误后再删掉调试入口。6.3 演示顺序建议演示时先启动程序点“开始”展示初始两张牌然后点“要牌”两次说明点数累加逻辑再点“停牌”展示庄家异步要牌最后出结果。中间最好有一局故意要牌爆掉展示失败状态和按钮禁用逻辑。整个流程控制在三分钟内。另外把第2章的handValue单测结果截图放进课设报告比贴十张界面截图更能证明工作量也更能回答老师“你这个规则靠谱吗”的提问。Qt这个方向在找工作面试里也常被问到能讲清楚状态机、布局、信号槽这三大件比做几个炫酷动画更扎实。我自己当初做课设时把逻辑层和界面层完全分离后面改需求时省了大量时间希望这篇文章能帮你的课设少踩几个坑一次通过。本文还有配套的精品资源点击获取
返回列表