First Commit

This commit is contained in:
xiaolinquan committed 2026-11-06 01:04:35 +08:00
commit 3796ec5cd2
146 files changed
+4825

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.16)
project(RaffleGroups VERSION 1.0 LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
if(QT_VERSION_MAJOR EQUAL 5 AND Qt5Core_VERSION VERSION_LESS 5.14.2)
message(FATAL_ERROR "Qt 5.14.2 or newer is required.")
endif()
if(QT_VERSION_MAJOR EQUAL 6)
set(CMAKE_CXX_STANDARD 17)
endif()
if(MSVC)
add_compile_options(/utf-8)
else()
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
add_executable(RaffleGroups WIN32 main.cpp appearance.h mainwindow.cpp mainwindow.h roster.cpp roster.h mainwindow.ui)
target_link_libraries(RaffleGroups PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
option(BUILD_TEACHING_TESTS "Build teaching project tests" OFF)
if(BUILD_TEACHING_TESTS)
enable_testing()
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
add_executable(test_RaffleGroups tests/test_RaffleGroups.cpp mainwindow.cpp mainwindow.h
roster.cpp roster.h mainwindow.ui)
target_include_directories(test_RaffleGroups PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(test_RaffleGroups PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Test)
add_test(NAME RaffleGroups COMMAND test_RaffleGroups -platform offscreen)
endif()
+65
View File
@@ -0,0 +1,65 @@
# 项目三:随机抽签与分组工具
适合第3—4章讲解 QStringList、QSet、随机数、下拉框和按钮状态。
## 名单的含义
每行代表一位学生。行首和行尾空白会被去掉,空行会被忽略。相同条目不允许重复;同名学生可以写成“01 张三”和“02 张三”。程序不根据姓名猜测是否为同一个人。
打开程序时填入一份虚构的 12 人示例名单,但还没有应用。检查后点击“应用名单”,才能抽签和分组。每次应用都会开始新一轮并清空旧结果。修改名单后,操作按钮暂时禁用,避免操作的实际名单与编辑框不一致。
名单上限为 5000 人,每条最多 100 个字符。这个限制用于及时发现错误粘贴,普通班级无需调整。
## 不重复抽签
选择本次人数后点击“抽取”。程序保存两份列表:完整名单和剩余候选池。抽到一位学生后,立即从候选池中移除,后续抽签只能从剩余的人中选择。
本轮全部抽完后,抽取按钮不可用。点击“重置抽签”会重新填满候选池并清除抽签记录;已生成的分组保持不变。
课堂上可以先用 A、B、C 三人演示:抽出 B 后候选池只剩 A、C。让学生解释为什么不能每次都从完整名单随机选择。
## 随机分组
分组始终使用全部已应用名单,与抽签结果无关。
有两种参数:
- 每组最多人数:先用向上取整计算需要几组,再平均分配。
- 分成几组:直接指定组数,组数不能超过人数。
例如 12 人、每组最多 5 人,需要 3 组。为了平衡人数,结果是每组 4 人。这个参数表示人数上限,不要求先装满一组再分下一组。
程序先用 Fisher–Yates 算法打乱名单,再按组号轮流分配。每人恰好出现一次,各组人数最多相差 1。再次点击“生成分组”会重新分组,结果不保证与上次不同。
## 课堂任务
第一步只读名单,使用 QStringList 保存,显示有效人数。再加入空行过滤和重复检查,观察同名但学号不同的两条记录应怎样处理。
第二步实现抽签:先抽一人,再抽多人。让学生跟踪 takeAt 执行前后候选池的大小。
第三步实现分组。先按原顺序轮流分组,确认没有遗漏,再加入洗牌。把“随机顺序”和“完整分配”分开验证,便于找错。
第四步处理界面状态:名单编辑后禁用操作,应用成功后启用,名单重复时保留错误原因。
## 学生练习
1. 增加“复制本轮抽签记录”按钮。
2. 保存分组结果到文本文件,作为第8章的延伸任务。
3. 增加“撤销上一次抽签”,明确如何把人员放回候选池,以及怎样修改历史记录。
## 验收用例
| 输入或操作 | 预期 |
|---|---|
| 空名单 | 显示提示,不允许开始 |
| A 与前后带空格的 A | 判断重复并指出行号 |
| 01 张三、02 张三 | 作为两位学生接受 |
| 4 人分两次各抽 2 人 | 四人各出现一次,随后不能继续抽 |
| 12 人,每组最多 5 人 | 3 组,每组 4 人 |
| 10 人,分成 3 组 | 人数为 4、3、3,顺序可变 |
| 3 人,要求分成 4 组 | 显示组数过多的提示 |
| 先抽签,再分组 | 分组仍包含全部名单 |
| 已生成结果后编辑名单 | 旧结果清空,重新应用前不能操作 |
自动化测试用固定种子得到可重复的随机序列,并对 1—100 人的两种分组方式检查总人数、唯一性和组间人数差。界面使用全局随机生成器,正常抽签不使用固定种子。
+10
View File
@@ -0,0 +1,10 @@
QT += widgets
CONFIG += c++14
greaterThan(QT_MAJOR_VERSION, 5): CONFIG += c++17
TEMPLATE = app
TARGET = RaffleGroups
SOURCES += main.cpp mainwindow.cpp roster.cpp
HEADERS += mainwindow.h roster.h
FORMS += mainwindow.ui
msvc: QMAKE_CXXFLAGS += /utf-8
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <QApplication>
#include <QFont>
#include <QStyleFactory>
inline void applyAppearance(QApplication &app)
{
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
app.setFont(QFont(QStringLiteral("Microsoft YaHei UI"), 11));
app.setStyleSheet(QStringLiteral(
"QMainWindow { background: #f5f7fb; }"
"QLabel { color: #23364c; }"
"QLabel#titleLabel { font-size: 27px; font-weight: bold; color: #173b63; }"
"QLabel#subtitleLabel, QLabel#helpLabel { color: #52657a; }"
"QGroupBox { background: white; border: 1px solid #d6e0eb;"
" border-radius: 8px; margin-top: 12px; padding: 18px 12px 12px; }"
"QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; }"
"QLineEdit, QSpinBox, QComboBox { min-height: 32px; padding: 3px 8px; }"
"QLineEdit, QSpinBox, QComboBox, QPlainTextEdit, QListWidget"
" { background: white; color: #23364c; }"
"QPushButton { min-height: 34px; padding: 4px 14px; }"
"QPushButton#convertButton, QPushButton#startButton, QPushButton#drawButton,"
" QPushButton#applyButton, QPushButton#groupsButton { background: #255c93;"
" color: white; border: 1px solid #255c93; border-radius: 5px; }"
"QPushButton:disabled, QPushButton#convertButton:disabled,"
" QPushButton#startButton:disabled, QPushButton#drawButton:disabled,"
" QPushButton#applyButton:disabled, QPushButton#groupsButton:disabled"
" { color: #8995a3; background: #e6ebf1; border-color: #d6e0eb; }"
"QLabel#statusLabel { color: #185376; }"
"QLineEdit#resultEdit { font-size: 23px; min-height: 45px; color: #175e53; }"
"QLabel#timeLabel { font: bold 76px 'Consolas'; color: #173b63; }"
"QProgressBar { border: 1px solid #d6e0eb; border-radius: 6px; height: 14px; }"
"QProgressBar::chunk { background: #2b8a7d; border-radius: 5px; }"));
}
+19
View File
@@ -0,0 +1,19 @@
#include "mainwindow.h"
#include "appearance.h"
#include <QApplication>
#include <QFont>
#include <QStyleFactory>
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
QApplication app(argc, argv);
applyAppearance(app);
MainWindow window;
window.show();
return app.exec();
}
+121
View File
@@ -0,0 +1,121 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QApplication>
#include <QClipboard>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->groupModeCombo->addItems({tr("每组最多人数"), tr("分成几组")});
connect(ui->namesEdit, &QPlainTextEdit::textChanged, this, &MainWindow::markDirty);
connect(ui->applyButton, &QPushButton::clicked, this, &MainWindow::applyNames);
connect(ui->sampleButton, &QPushButton::clicked, this, [this] {
ui->namesEdit->setPlainText(QStringLiteral(
"01 陈晨\n02 林悦\n03 王宁\n04 李然\n05 周航\n06 赵晴\n"
"07 孙瑞\n08 吴桐\n09 郑宇\n10 许诺\n11 何欣\n12 方舟"));
});
connect(ui->drawButton, &QPushButton::clicked, this, &MainWindow::drawNames);
connect(ui->groupsButton, &QPushButton::clicked, this, &MainWindow::makeGroups);
connect(ui->resetButton, &QPushButton::clicked, this, [this] {
m_session.resetDraw();
m_round = 0;
ui->historyList->clear();
ui->chosenEdit->clear();
ui->statusLabel->setText(tr("抽签已重置,所有人重新进入候选池。"));
refreshControls();
});
connect(ui->copyGroupsButton, &QPushButton::clicked, this, [this] {
QApplication::clipboard()->setText(ui->groupsEdit->toPlainText());
ui->statusLabel->setText(tr("分组结果已复制。"));
});
connect(ui->groupModeCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this, [this] { ui->groupsEdit->clear(); ui->copyGroupsButton->setEnabled(false); });
connect(ui->groupValueSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
this, [this] { ui->groupsEdit->clear(); ui->copyGroupsButton->setEnabled(false); });
ui->sampleButton->click();
}
MainWindow::~MainWindow() { delete ui; }
void MainWindow::markDirty()
{
m_dirty = true;
// 名单有改动时清除旧展示,避免把旧名单的结果当成新名单的结果。
ui->chosenEdit->clear();
ui->historyList->clear();
ui->groupsEdit->clear();
ui->copyGroupsButton->setEnabled(false);
ui->statusLabel->setText(tr("名单尚未应用。检查后点击“应用名单”开始新一轮。"));
refreshControls();
}
void MainWindow::applyNames()
{
const auto parsed = Roster::parse(ui->namesEdit->toPlainText());
if (!parsed.ok()) {
ui->statusLabel->setText(parsed.error);
return;
}
m_session.setNames(parsed.names);
m_dirty = false;
m_round = 0;
ui->historyList->clear();
ui->chosenEdit->clear();
ui->groupsEdit->clear();
ui->copyGroupsButton->setEnabled(false);
ui->statusLabel->setText(tr("已应用 %1 人。抽签与分组现在使用这份名单。").arg(parsed.names.size()));
refreshControls();
}
void MainWindow::refreshControls()
{
const int remaining = m_session.remainingCount();
ui->drawButton->setEnabled(!m_dirty && remaining > 0);
ui->resetButton->setEnabled(!m_dirty);
ui->groupsButton->setEnabled(!m_dirty && !m_session.names().isEmpty());
ui->drawCountSpin->setEnabled(!m_dirty && remaining > 0);
ui->drawCountSpin->setMaximum(qMax(1, remaining));
ui->countLabel->setText(m_dirty ? tr("名单待应用")
: tr("名单 %1 人 · 尚未抽到 %2 人").arg(m_session.names().size()).arg(remaining));
}
void MainWindow::drawNames()
{
if (m_dirty) return;
const QStringList selected = m_session.draw(ui->drawCountSpin->value(),
*QRandomGenerator::global());
if (selected.isEmpty()) return;
++m_round;
ui->chosenEdit->setPlainText(selected.join(QStringLiteral("\n")));
ui->historyList->addItem(tr("第 %1 次:%2").arg(m_round).arg(selected.join(QStringLiteral("、"))));
ui->historyList->scrollToBottom();
ui->statusLabel->setText(m_session.remainingCount() == 0
? tr("所有人都已抽到。点击“重置抽签”可以开始下一轮。")
: tr("本次抽取 %1 人,已从候选池中移除。").arg(selected.size()));
refreshControls();
}
void MainWindow::makeGroups()
{
if (m_dirty) return;
const auto groups = m_session.groups(ui->groupValueSpin->value(),
ui->groupModeCombo->currentIndex() == 1, *QRandomGenerator::global());
if (groups.isEmpty()) {
ui->groupsEdit->clear();
ui->copyGroupsButton->setEnabled(false);
ui->statusLabel->setText(tr("组数不能超过名单人数,请调整后再分组。"));
return;
}
QStringList lines;
for (int i = 0; i < groups.size(); ++i) {
lines.append(tr("第 %1 组(%2 人)").arg(i + 1).arg(groups.at(i).size()));
lines.append(groups.at(i).join(QStringLiteral("、")));
lines.append(QString());
}
ui->groupsEdit->setPlainText(lines.join(QStringLiteral("\n")));
ui->copyGroupsButton->setEnabled(true);
ui->statusLabel->setText(tr("已将全部 %1 人分为 %2 组,每人出现一次,各组人数最多相差 1。")
.arg(m_session.names().size()).arg(groups.size()));
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QMainWindow>
#include "roster.h"
namespace Ui { class MainWindow; }
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
private:
Ui::MainWindow *ui;
Roster::Session m_session;
bool m_dirty = true;
int m_round = 0;
void markDirty();
void applyNames();
void refreshControls();
void drawNames();
void makeGroups();
};
+329
View File
@@ -0,0 +1,329 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="windowTitle">
<string>随机抽签与分组工具</string>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1120</width>
<height>780</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="rootLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>24</number>
</property>
<property name="topMargin">
<number>24</number>
</property>
<property name="rightMargin">
<number>24</number>
</property>
<property name="bottomMargin">
<number>24</number>
</property>
<item>
<widget class="QLabel" name="titleLabel">
<property name="text">
<string>随机抽签与分组</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="subtitleLabel">
<property name="text">
<string>先应用名单,再抽签或分组。同名学生请使用“学号 姓名”区分。</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="countLabel">
<property name="text">
<string>名单待应用</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="bodyLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QGroupBox" name="namesGroup">
<property name="title">
<string>班级名单</string>
</property>
<property name="minimumSize">
<size>
<width>310</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="namesLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QLabel" name="namesHelp">
<property name="text">
<string>每行一人,空行会被忽略。修改名单后需重新应用。</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="namesEdit">
<property name="placeholderText">
<string>01 张三
02 李四</string>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>320</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyButton">
<property name="text">
<string>应用名单(开始新一轮)</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sampleButton">
<property name="text">
<string>填入示例名单</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="resultTabs">
<property name="minimumSize">
<size>
<width>570</width>
<height>0</height>
</size>
</property>
<widget class="QWidget" name="drawTab">
<attribute name="title">
<string>不重复抽签</string>
</attribute>
<layout class="QVBoxLayout" name="drawLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<layout class="QHBoxLayout" name="drawControls">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QLabel" name="drawCountLabel">
<property name="text">
<string>本次抽取人数</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="drawCountSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="drawButton">
<property name="text">
<string>抽取</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="chosenLabel">
<property name="text">
<string>本次抽取</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="chosenEdit">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>145</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="historyLabel">
<property name="text">
<string>本轮抽签记录</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="historyList" />
</item>
<item>
<widget class="QPushButton" name="resetButton">
<property name="text">
<string>重置抽签</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="groupTab">
<attribute name="title">
<string>随机分组</string>
</attribute>
<layout class="QVBoxLayout" name="groupLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QLabel" name="groupHelp">
<property name="text">
<string>分组使用全部已应用名单,与已抽到哪些人无关。每次点击都会重新分组。</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="groupControls">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QComboBox" name="groupModeCombo" />
</item>
<item>
<widget class="QSpinBox" name="groupValueSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="value">
<number>4</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="groupsButton">
<property name="text">
<string>生成分组</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="groupsEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyGroupsButton">
<property name="text">
<string>复制分组结果</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string />
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>55</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources />
<connections />
</ui>
+72
View File
@@ -0,0 +1,72 @@
#include "roster.h"
#include <QSet>
namespace Roster {
ParsedNames parse(const QString &text)
{
ParsedNames result;
QSet<QString> seen;
const QStringList lines = text.split(QLatin1Char('\n'));
int lineNumber = 0;
for (const QString &line : lines) {
++lineNumber;
const QString name = line.trimmed();
if (name.isEmpty()) continue;
if (seen.contains(name)) {
result.error = QStringLiteral("第 %1 行与前面的条目重复:%2。"
"同名学生请加上学号后再应用名单。").arg(lineNumber).arg(name);
return result;
}
if (name.size() > 100) {
result.error = QStringLiteral("第 %1 行超过 100 个字符,请检查是否粘贴了整段文字。")
.arg(lineNumber);
return result;
}
seen.insert(name);
result.names.append(name);
}
if (result.names.isEmpty())
result.error = QStringLiteral("名单为空,请每行输入一位学生。");
else if (result.names.size() > 5000)
result.error = QStringLiteral("名单超过 5000 人,请分批操作。");
return result;
}
void Session::setNames(const QStringList &names)
{
m_names = names;
resetDraw();
}
void Session::resetDraw() { m_pool = m_names; }
QStringList Session::draw(int count, QRandomGenerator &random)
{
QStringList selected;
if (count <= 0 || count > m_pool.size()) return selected;
for (int i = 0; i < count; ++i) {
const int index = random.bounded(static_cast<int>(m_pool.size()));
// 从候选池中移除已抽到的人,本轮后续抽签就不会重复。
selected.append(m_pool.takeAt(index));
}
return selected;
}
QList<QStringList> Session::groups(int value, bool valueIsGroupCount,
QRandomGenerator &random) const
{
QList<QStringList> result;
const int n = static_cast<int>(m_names.size());
if (n == 0 || value <= 0 || (valueIsGroupCount && value > n)) return result;
const int count = valueIsGroupCount ? value : (n + value - 1) / value;
QStringList shuffled = m_names;
// Fisher–Yates 洗牌。每次从尚未固定的位置中均匀选取一个。
for (int i = n - 1; i > 0; --i)
qSwap(shuffled[i], shuffled[random.bounded(i + 1)]);
for (int i = 0; i < count; ++i) result.append(QStringList());
// 轮流放入各组,保证每人出现一次,各组人数最多相差 1。
for (int i = 0; i < n; ++i) result[i % count].append(shuffled.at(i));
return result;
}
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <QStringList>
#include <QList>
#include <QRandomGenerator>
namespace Roster {
struct ParsedNames {
QStringList names;
QString error;
bool ok() const { return error.isEmpty(); }
};
ParsedNames parse(const QString &text);
class Session {
public:
void setNames(const QStringList &names);
void resetDraw();
const QStringList &names() const { return m_names; }
int remainingCount() const { return static_cast<int>(m_pool.size()); }
QStringList draw(int count, QRandomGenerator &random);
QList<QStringList> groups(int value, bool valueIsGroupCount,
QRandomGenerator &random) const;
private:
QStringList m_names;
QStringList m_pool;
};
}
@@ -0,0 +1,13 @@
01 陈晨
02 林悦
03 王宁
04 李然
05 周航
06 赵晴
07 孙瑞
08 吴桐
09 郑宇
10 许诺
11 何欣
12 方舟
@@ -0,0 +1,119 @@
#include <QtTest>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QLabel>
#include <QSet>
#include "roster.h"
#include "mainwindow.h"
#include <QApplication>
#include <QDir>
#include "appearance.h"
static void savePreview(QWidget &window, const QString &fileName)
{
const QString folder = QString::fromLocal8Bit(qgetenv("QT_TEACHING_SCREENSHOTS"));
if (folder.isEmpty()) return;
QDir().mkpath(folder);
window.show();
QApplication::processEvents();
QVERIFY(window.grab().save(QDir(folder).filePath(fileName)));
}
class RaffleTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() { applyAppearance(*qApp); }
void parsing()
{
auto parsed = Roster::parse(QStringLiteral(" 01 张三 \r\n\n02 李四\r\n"));
QVERIFY(parsed.ok()); QCOMPARE(parsed.names.size(), 2);
QCOMPARE(parsed.names.first(), QStringLiteral("01 张三"));
QVERIFY(!Roster::parse(QStringLiteral("张三\n 张三 ")).ok());
QVERIFY(!Roster::parse(QStringLiteral(" \n ")).ok());
QVERIFY(Roster::parse(QStringLiteral("01 张三\n02 张三")).ok());
QVERIFY(!Roster::parse(QString(101, QLatin1Char('x'))).ok());
}
void drawingWithoutReplacement()
{
Roster::Session session;
const QStringList names = {QStringLiteral("A"), QStringLiteral("B"),
QStringLiteral("C"), QStringLiteral("D")};
session.setNames(names);
QRandomGenerator random(12345);
QVERIFY(session.draw(5, random).isEmpty());
QCOMPARE(session.remainingCount(), 4);
QStringList drawn = session.draw(2, random);
drawn.append(session.draw(2, random));
QSet<QString> unique;
for (const QString &name : drawn) unique.insert(name);
QCOMPARE(unique.size(), 4);
QCOMPARE(session.remainingCount(), 0);
QVERIFY(session.draw(1, random).isEmpty());
session.resetDraw();
QCOMPARE(session.remainingCount(), 4);
}
void groupingInvariants()
{
QRandomGenerator random(54321);
for (int n = 1; n <= 100; ++n) {
QStringList names;
for (int i = 0; i < n; ++i) names.append(QString::number(i));
Roster::Session session; session.setNames(names);
session.draw(1, random); // 分组仍使用全部名单。
for (bool byCount : {false, true}) {
const int value = byCount ? qMin(n, 7) : 7;
const auto groups = session.groups(value, byCount, random);
QCOMPARE(groups.size(), byCount ? value : (n + value - 1) / value);
QSet<QString> all; int total = 0, smallest = n, largest = 0;
for (const QStringList &group : groups) {
QVERIFY(!group.isEmpty());
smallest = qMin(smallest, static_cast<int>(group.size()));
largest = qMax(largest, static_cast<int>(group.size()));
total += static_cast<int>(group.size());
for (const QString &name : group) all.insert(name);
}
QCOMPARE(total, n); QCOMPARE(all.size(), n);
QVERIFY(largest - smallest <= 1);
if (!byCount) QVERIFY(largest <= value);
}
QVERIFY(session.groups(n + 1, true, random).isEmpty());
}
}
void uiWorkflow()
{
MainWindow w; w.show();
auto *names = w.findChild<QPlainTextEdit *>(QStringLiteral("namesEdit"));
auto *apply = w.findChild<QPushButton *>(QStringLiteral("applyButton"));
auto *draw = w.findChild<QPushButton *>(QStringLiteral("drawButton"));
auto *chosen = w.findChild<QPlainTextEdit *>(QStringLiteral("chosenEdit"));
QVERIFY(!draw->isEnabled());
QTest::mouseClick(apply, Qt::LeftButton);
QVERIFY(draw->isEnabled());
QTest::mouseClick(draw, Qt::LeftButton);
QVERIFY(!chosen->toPlainText().isEmpty());
savePreview(w, QStringLiteral("03_raffle.png"));
w.findChild<QTabWidget *>(QStringLiteral("resultTabs"))->setCurrentIndex(1);
QTest::mouseClick(w.findChild<QPushButton *>(QStringLiteral("groupsButton")), Qt::LeftButton);
auto *groups = w.findChild<QPlainTextEdit *>(QStringLiteral("groupsEdit"));
QVERIFY(groups->toPlainText().contains(QStringLiteral("第 3 组")));
savePreview(w, QStringLiteral("03_groups.png"));
names->setPlainText(QStringLiteral("A\nA"));
QVERIFY(!draw->isEnabled());
QVERIFY(groups->toPlainText().isEmpty());
QTest::mouseClick(apply, Qt::LeftButton);
QVERIFY(w.findChild<QLabel *>(QStringLiteral("statusLabel"))->text().contains(QStringLiteral("重复")));
names->setPlainText(QStringLiteral("A\nB"));
QTest::mouseClick(apply, Qt::LeftButton);
w.findChild<QTabWidget *>(QStringLiteral("resultTabs"))->setCurrentIndex(0);
w.findChild<QSpinBox *>(QStringLiteral("drawCountSpin"))->setValue(2);
QTest::mouseClick(draw, Qt::LeftButton);
QVERIFY(!draw->isEnabled());
QTest::mouseClick(w.findChild<QPushButton *>(QStringLiteral("resetButton")), Qt::LeftButton);
QVERIFY(draw->isEnabled());
}
};
QTEST_MAIN(RaffleTest)
#include "test_RaffleGroups.moc"
@@ -0,0 +1,11 @@
QT += widgets testlib
CONFIG += c++14 console testcase
greaterThan(QT_MAJOR_VERSION, 5): CONFIG += c++17
TEMPLATE = app
TARGET = test_RaffleGroups
INCLUDEPATH += ..
SOURCES += test_RaffleGroups.cpp ../mainwindow.cpp ../roster.cpp
HEADERS += ../mainwindow.h ../roster.h
FORMS += ../mainwindow.ui
msvc: QMAKE_CXXFLAGS += /utf-8