简介基于C与Qt框架实现的分角色登录学生信息管理系统课程设计源码面向计算机科学、软件工程、信息安全、大数据、人工智能等专业的在校学生和教师可用于期末大作业、课程设计或毕业设计初期方案演示。项目围绕“分角色登录”展开区分教师与学生两类身份演示登录权限控制、学生信息增删改查等管理流程涉及Qt信号槽、界面布局、文件读写等常用技术点代码结构清晰具备较好的拓展空间。压缩包共25个文件涵盖cpp源文件、h头文件、ui界面文件、pro工程配置等核心类型并附带演示gif、图标图片、说明文档等素材整体仅658KB轻量完整便于快速查看和运行验证。目前已有183人学习下载代码经过功能验证可稳定运行。无论是作为Qt初学者的入门进阶项目还是作为课程设计、项目立项的参考原型都很有参考价值也支持二次开发以满足更复杂的业务需求。1. 一个 C/Qt 学生信息管理系统的 demo值钱的地方在角色边界分角色登录的学生信息管理系统是 C/Qt 学习路径上最常见的完整 demo有数据库、有界面、有登录态、有增删改查。但网上大部分同名源码把角色做成登录界面的一个下拉框登录成功之后所有按钮都亮着。真正区分管理员、教师、学生三种身份的是登录成功那一刻主窗口里挂载了哪些界面、哪些操作被禁用以及核心业务函数里是否还留了第二道权限检查。这篇文章按常见做法拆解这个项目从表结构建模开始到 Qt Widgets 登录窗口、QStackedWidget 的角色化主界面、QSqlTableModel 的学生信息管理最后落到这类 demo 打包发布最容易翻车的三个位置。适合正在写 Qt 课程设计或第一份项目代码的人也适合拿这个项目把 C/Qt 的基础知识点重新顺一遍的人。2. 分角色登录的角色模型与建表策略登录查询在 SQL 端完成分角色登录的第一步不是写界面而是定业务规则系统里有哪几类角色每类角色能看哪些页面、改哪些数据。规则定清楚表结构和 C 代码才有依据。2.1 用户表直接存 role 字段的问题很多 demo 的做法是在 users 表里加一个 TEXT 类型的 role 字段值直接写 admin、teacher、student。这种做法在功能上能跑但它把角色的“定义”和“归属”混在了一张表里。想要增加一个“辅导员只读账号”就得改 C 代码里所有判断字符串的地方重新编译想要调整某个角色能访问的页面同样要动代码。常见做法是拆成角色表和用户表用户表通过外键指向角色表。这样角色的增删改查变成纯数据操作C 代码只认 role_id 或 role_name权限调整不需要重新编译。对于课程设计和项目复盘这个拆法本身就是加分项。2.2 角色表、用户表、学生表的最小建表 SQL这个项目用 SQLite 最合适单文件、零配置、随压缩包分发就能跑。表结构需要三张核心表roles、users、students。学生信息挂在独立的 students 表里和账号体系分开因为一个学生账号对应一条学生档案但教师账号不产生学生档案。PRAGMA foreign_keys ON; CREATE TABLE roles ( role_id INTEGER PRIMARY KEY AUTOINCREMENT, role_name TEXT NOT NULL UNIQUE, description TEXT ); CREATE TABLE users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role_id INTEGER NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (role_id) REFERENCES roles(role_id) ); CREATE TABLE students ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, gender TEXT DEFAULT 未知, class_name TEXT, major TEXT, grade TEXT, phone TEXT ); INSERT INTO roles(role_id, role_name, description) VALUES (1, admin, 管理员拥有全部权限), (2, teacher, 教师可查询与导出), (3, student, 学生仅查询本人信息); INSERT INTO users(username, password_hash, role_id) VALUES (admin, admin123, 1), (teacher01, 123456, 2), (student01, 123456, 3);students 表的主键用 TEXT 而不是 INTEGER因为学号在不同学校可能包含字母前缀用自增整数主键会在导入外部数据时产生冲突。users 表里 demo 阶段密码先存明文方便跑通流程第 5 章会给出替换为 SHA-256 哈希的最小改法。2.3 登录查询在 SQL 端一次查出角色登录验证的常见错误是先用用户名查出用户记录在 C 里比较密码再查一次角色表拿角色名。两次查询中间如果第一次查询命中了、第二次失败程序还得处理不一致状态。更可靠的方式是把校验和角色获取合并成一条 JOIN 查询用户名和密码同时匹配才返回记录否则返回空结果。这样登录成功时角色名已经握在手里。bool LoginDialog::tryLogin(const QString username, const QString password) { QSqlQuery query(m_db); query.prepare(SELECT u.user_id, r.role_name FROM users u JOIN roles r ON u.role_id r.role_id WHERE u.username :username AND u.password_hash :password); query.bindValue(:username, username); query.bindValue(:password, password); if (query.exec() query.next()) { m_session.userId query.value(0).toInt(); m_session.username username; m_session.roleName query.value(1).toString(); return true; } return false; }这里用preparebindValue而不是直接拼接字符串是为了避免 SQL 注入。用户名里的单引号如果直接拼进 SQL会破坏语句结构绑定参数后Qt 驱动会处理转义。query.value(0)拿到的是 user_idvalue(1)是 JOIN 出来的 role_name。角色权限矩阵可以先在文档里定清楚这个表同样适用于后面菜单裁剪操作adminteacherstudent学生信息增删改允许禁止禁止查询全部学生允许允许禁止查询本人信息允许允许允许导入 / 导出允许允许禁止用户管理允许禁止禁止2.4 登录态在 Qt 进程内的传递UserSession 与全局访问点登录成功后主窗口、学生管理页、菜单控制都需要知道“当前是谁、什么角色”。把 user_id、username、roleName 散落在各个窗口的成员变量里会让代码到处传递参数。常见做法是定义一个 UserSession 结构体再用一个全局访问点保存当前会话。Qt 里可以写成普通全局变量也可以用 Q_GLOBAL_STATIC 包一层避免静态初始化顺序问题。struct UserSession { int userId 0; QString username; QString roleName; bool isValid() const { return userId 0; } }; Q_DECLARE_METATYPE(UserSession) Q_GLOBAL_STATIC(UserSession, currentSession) UserSession *session() { return currentSession(); }Q_DECLARE_METATYPE让 UserSession 可以作为信号参数跨线程传递后续如果要把登录状态通过信号发给子窗口不会报 “Unknown parameter type” 错误。各个业务窗口统一通过session()-roleName判断权限比每个窗口各自保存一份用户信息更可控。3. 用 Qt Widgets 实现登录窗口与角色主界面切换登录窗口是用户看到的第一个界面它承担的不只是“输入账号密码”还要处理回车提交、失败次数限制、以及登录成功后的界面交接。3.1 登录对话框的控件组织与回车键处理登录对话框继承 QDialog控件部分放在 ui 文件里。布局用 QFormLayout 竖向排列用户名输入框、密码输入框、错误提示标签底部放两个按钮。密码框必须设置setEchoMode(QLineEdit::Password)否则输入内容会明文显示。控件关键属性作用usernameEditQLineEdit接收用户名passwordEditEchoMode Password接收密码不回显loginBtnsetDefault(true)点击触发登录回车键也会触发cancelBtnQPushButton关闭对话框statusLabelQLabel setStyleSheet显示错误信息loginBtn-setDefault(true)是一个容易被忽略的细节。它让 QDialog 在用户按下回车键时优先触发登录按钮而不是默认的取消按钮。登录框没有回车提交体验会差一大截。3.2 槽函数返回值在 connect 里会被丢弃登录结果靠 exec() 传递刚接触 Qt 的人容易写一个bool onLoginClicked()想在 connect 里拿到验证结果。信号槽机制里槽函数的返回值会被忽略即使使用函数指针形式的 connect也不会按普通函数调用的方式把返回值传回给信号发射方。正确做法是让点击槽函数内部完成验证验证结果决定 QDialog 以哪个状态退出然后调用方通过exec()的返回值接手。void LoginDialog::onLoginClicked() { const QString username ui-usernameEdit-text().trimmed(); const QString password ui-passwordEdit-text(); if (username.isEmpty() || password.isEmpty()) { ui-statusLabel-setText(用户名和密码不能为空); return; } if (tryLogin(username, password)) { accept(); // 登录成功让 exec() 返回 QDialog::Accepted } else { handleLoginFailed(); } }入口代码在 main 函数或启动窗口中调用LoginDialog dlg(db); if (dlg.exec() QDialog::Accepted) { *session() dlg.session(); openMainWindow(dlg.session()); } else { QApplication::exit(1); }dlg.session()返回的是登录成功时填充好的 UserSession赋值给全局session()后主窗口直接读取角色名。exec()启动一个模态事件循环登录按钮的点击、回车触发都在这个循环里处理不会阻塞界面绘制。3.3 连续失败三次后禁用登录按钮的 QTimer 实现登录失败次数限制是这类系统的常规要求最小实现是“失败三次禁用 15 秒”。用一个 int 成员变量做计数器达到阈值后禁用按钮再通过 QTimer::singleShot 在指定时间后恢复。void LoginDialog::handleLoginFailed() { m_failCount; ui-statusLabel-setText(QString(用户名或密码错误剩余尝试次数%1) .arg(3 - m_failCount)); if (m_failCount 3) { ui-loginBtn-setEnabled(false); ui-statusLabel-setText(连续失败 3 次请在 15 秒后重试); QTimer::singleShot(15000, this, [this]() { ui-loginBtn-setEnabled(true); m_failCount 0; ui-statusLabel-clear(); }); } }里用 lambda 捕获 this15 秒后把按钮恢复。m_failCount是 LoginDialog 的成员变量不要用局部静态变量否则每次点击都会重新初始化。如果需要更严格的安全策略可以把这个计数放到数据库或配置文件里防止重启程序绕过限制但 demo 阶段窗体内存计数足够。3.4 按角色装配主窗口QStackedWidget 只挂当前角色需要的页登录成功后的主窗口不适合用“一个窗口加一堆按钮”的方式写。常见做法是用 QStackedWidget 做页面容器每种角色对应一组页面按角色动态装载。void MainWindow::setupPagesByRole(const UserSession s) { auto *stack new QStackedWidget(this); if (s.roleName admin) { stack-addWidget(createUserManagePage()); // 用户管理 stack-addWidget(createStudentManagePage()); // 学生信息管理 } else if (s.roleName teacher) { stack-addWidget(createStudentViewPage()); // 只读学生列表 } else { stack-addWidget(createStudentSelfPage()); // 仅本人信息 } setCentralWidget(stack); }这样每类角色拿到的主窗口结构完全不同。admin 需要用户管理和学生管理两个页面teacher 只有只读视图student 只看自己的档案。createStudentViewPage和createStudentManagePage内部可以复用同一个学生列表组件只是初始化参数不同比如是否允许编辑、是否默认筛选当前学号。4. QSqlTableModel 实现学生信息管理以及按角色裁剪菜单学生信息管理的核心是“表格视图 数据库表”的映射。Qt 里最直接的做法是 QTableView 配 QSqlTableModel一张表的增删改查不需要手写 SQL 模板就能跑起来。4.1 学生表映射到 QTableView 的最小代码把 students 表加载到表格视图只需要几行代码。注意 QSqlTableModel 构造时传入数据库连接避免操作到默认连接上别的业务表。QSqlDatabase db QSqlDatabase::database(student_mgr); auto *model new QSqlTableModel(this, db); model-setTable(students); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-setHeaderData(0, Qt::Horizontal, 学号); model-setHeaderData(1, Qt::Horizontal, 姓名); model-select(); ui-tableView-setModel(model); ui-tableView-horizontalHeader()-setStretchLastSection(true);setTable只是指定了表名真正的数据加载发生在select()之后。setHeaderData用于把数据库字段名换成用户看得懂的列名否则表格列头会直接显示 student_id、class_name。horizontalHeader()-setStretchLastSection(true)让最后一列撑满窗口宽度避免表格右侧留白。由于默认连接可能被其他模块占用建议在程序启动时统一用命名连接QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE, student_mgr); db.setDatabaseName(dbPath); if (!db.open()) { // 打印 db.lastError().text()不要静默失败 }4.2 增删改与提交策略OnManualSubmit 比 OnFieldChange 可控QSqlTableModel 有三种编辑策略差异体现在“修改什么时候真正写回数据库”编辑策略行为适用场景OnFieldChange字段值改变即提交单用户、字段少、不需要撤销OnRowChange切换到另一行时提交表单式编辑OnManualSubmit手动调用 submitAll() 才提交批量操作、可回滚、权限控制这个项目用 OnManualSubmit。它在用户改了表格内容后不会立刻落库点保存按钮时才统一提交。一旦发现权限不足或数据校验失败可以 revertAll() 撤销全部未提交修改。void StudentManageWidget::onSaveClicked() { if (session()-roleName ! admin) { QMessageBox::warning(this, 权限不足, 当前角色仅可查看学生信息); return; } if (!m_model-submitAll()) { QMessageBox::critical(this, 保存失败, m_model-lastError().text()); m_model-revertAll(); } }新增和删除分别走 insertRow 与 removeRow之后同样调用 submitAll 提交int row m_model-rowCount(); m_model-insertRow(row); m_model-setData(m_model-index(row, 0), ui-studentIdEdit-text()); m_model-setData(m_model-index(row, 1), ui-nameEdit-text());4.3 菜单和按钮按角色裁剪setVisible 与 setEnabled 的边界界面层裁剪是最直观的权限控制admin 看到“用户管理”菜单teacher 和 student 看不到只有 admin 能点击“添加学生”“删除学生”。区分点在于 setVisible 和 setEnabled 的使用时机。void MainWindow::applyRolePermissions(const QString roleName) { const bool isAdmin (roleName admin); const bool canEdit isAdmin; m_addAction-setEnabled(canEdit); m_editAction-setEnabled(canEdit); m_delAction-setEnabled(canEdit); m_userManageAction-setVisible(isAdmin); m_importAction-setEnabled(roleName admin || roleName teacher); }setEnabled(false)表示菜单能看到但点不动适合保留功能入口提示用户“没有权限”。setVisible(false)直接把菜单隐藏适合用户管理这类与学生无关的功能。这两种控制都要配合业务函数里的二次校验界面隐藏只是防呆不防绕过。如果哪天某个按钮忘记裁剪二次校验还能兜底。4.4 批量导入时的 QProgressDialog 反馈学生信息超过几百条时逐条插入会造成界面短暂卡顿。QProgressDialog 是 Qt 内置的进度反馈控件配合 processEvents 保持界面响应。QProgressDialog progress(正在导入学生数据..., 取消, 0, records.size(), this); progress.setWindowModality(Qt::WindowModal); for (int i 0; i records.size(); i) { if (progress.wasCanceled()) { break; } insertStudentRecord(records.at(i)); progress.setValue(i 1); QCoreApplication::processEvents(); }setWindowModality(Qt::WindowModal)避免进度窗口挡住整个应用processEvents让 Qt 在处理循环中也能重绘窗口和响应取消操作。这段代码即使 demo 数据量不大也值得写进去它反映出对“长时间操作必须有反馈”的理解。5. 打包发布必踩的三个坑和这个 demo 最值得改的三处代码源码压缩包下载下来能跑不算结束把 exe 拷到别的机器还能跑才是完整的收尾。这里三个坑是 Qt 学生管理系统发布时最高发的。坑一windeployqt 打不全 SQLite 驱动。windeployqt 会复制 Qt 核心库和 platforms 插件但 QSQLITE 驱动经常被漏掉目标机器提示 “Driver not loaded”。在构建后的 release 目录执行windeployqt --release --no-translations StudentManager.exe手动确认 exe 所在目录下存在sqldrivers/qsqlite.dll没有就从 Qt 安装目录的 plugins 下复制。坑二QT_QPA_PLATFORM_PLUGIN_PATH 报错。运行时的典型错误是找不到平台插件或者提示指向开发机上的 Qt 安装路径。在 main 函数构造 QApplication 之前把插件目录显式指到 exe 旁边的 platformsqputenv(QT_QPA_PLATFORM_PLUGIN_PATH, (QCoreApplication::applicationDirPath() QStringLiteral(/platforms)).toUtf8());坑三数据库相对路径失效。可执行文件不在工作目录时student.db的路径会找不到。定位数据库文件用 applicationDirPathQString dbPath QCoreApplication::applicationDirPath() /data/student.db;这个项目最值得改的三处代码第一是密码明文换哈希登录时用QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256).toHex()存储与比较第二是把 2.1 节的权限矩阵从 if-else 里拆到配置文件启动时加载第三是给所有界面字符串包一层tr()哪怕不需要多语言也能避免因为编码问题重编。面试被追问这个项目时能把这五处取舍讲清楚比背一套 c 八股要扎实得多。本文还有配套的精品资源点击获取