2018级 NJU CS 程序设计基础实验 项目一:KTV点歌系统 这是一个类似于学生管理系统的项目,数据结构和算法在四个项目里是最简单的。有限制如下:
图形界面放在了拓展里,不作为基本要求。 在之前的寒假中,已经做了一个简(zhi)化(zhang)的“酒店管理系统”,先是实现了黑底控制台版,后面学点Qt 重做实现真的图形界面。 厌倦了黑底控制台的界面实现,直接选择使用Qt. 整个实现都是围绕着需求来的,那么先溜一波需求。 从课件ppt里选了两张: ![]() ![]() 管理员账号: 想过加密什么的,但是毕竟不会,感觉搞点小操作也没意思,就采取了最简单的方式,设置单个账号,管理员ID和密码直接输出到文件。 看同学展示发现是有人在账号这方面拓展的,比如游客与用户账号登录,还有超管什么的... 多窗口转换: 这是最开始很困扰的地方,菜得连这个都不会。 想法是开始窗口,选择进入管理员/用户窗口,退出后回到开始窗口 //main.cpp intmain(int argc, char *argv[]) { QApplication a(argc, argv); StartDialog *s = new StartDialog; s->show(); return a.exec(); }
//startdialog.h StartDialog::StartDialog() { ... connect(some_object, some_signal, this, &StartDialog::showUserWindow); ... }
//startdialog.cpp void StartDialog::showUserWindow() { UserWindow *w = new UserWindow; close(); w->show(); }
//userwindow.h classUserWindow : publicQMainWindow { ... protected: voidcloseEvent(QCloseEvent *event); ... };
//userwindow.cpp void UserWindow::closeEvent(QCloseEvent *ev) { songsModel->saveData(); StartDialog *s = new StartDialog; ev->accept(); s->show(); s->move((QApplication::desktop()->width() - s->width())/2,(QApplication::desktop()->height() - s->height())/2); //在屏幕中央显示 } Model/View: 核心采用Qt提供的model/view架构。同时使用QSortFilterProxyModel,排序和查询就不用写了。 struct Song { int id; //# 这是一个困惑的地方,因为按照上面的需求这就是在list中的下标(索引),感觉没必要 QString name; QString singerName; QString pinyinAbbr; float score; int scoredTimes; int playedTimes;
QUrl url; //# 后面为了播放音乐加的 };
//# 重载插入和提取运算符,之后便可对QList类型直接输入输出 QDataStream &operator<<(QDataStream &stream, const Song &song); QDataStream &operator>>(QDataStream &stream, Song &song);
class SongsModel : public QAbstractTableModel { private: QList<Song> songs; ...... }; 根据文档,重载 rowCount(), columnCount(), data(), headerData(), 以及insertRows(), removeRows() 其他的就不讲了. 起初我把SongsModel作为其他类的一个成员,而管理员类和用户类是分开的,用起来不方便,然后就创了个全局对象. 要求里提到了分页(上图),而用Qt的表格类是不需要做分页的(有滚动条),但是我也想做个分页跳转。 Qt没有直接提供分页,网上搜也没搜到,唯一搜到的是用来解决大数据响应慢的(虽然只显示一部分但Qt会加载全部数据),而那样的话就不是直接用model/view架构那么简单了。 最后在StackOverflow上我找到了我想要的——调用verticalScrollBar() 如此一来一个无用的分页跳转就能完成了. ![]() 完工后的管理员界面 底下填数字的框用了QSpinBox,当时想的是QSpinBox直接面向数字就用了,但是相关signal在这里要绕一下,而且右侧的上下键多余了。 要把spinBox 和 verticalScrollBard的value关联起来,并且是双向的,那么就有个问题。这两个对象能用的signal只有valueChanged,直接connect的话,其中一个valueChanged改变另一个的值,就会再次触发信号,造成无限循环。 解决方法很简单,在其中一个槽函数中,对要改变值的对象调用setEnabled(false),取消其对信号的反应,最后再setEnabled(false) pageSpinBox->setEnabled(false); pageSpinBox->setValue(some_val); pageSpinBox->setEnabled(true); 也可以用QLineEdit,利用信号editingFinished()就不用上面那样了,但是需要加输入判断。 歌曲导入: 该项目并不要求真的播放歌曲,我一开始也没想,但是做着做着就想用音乐文件了(主要是想起了音乐文件会附带相关信息)。所以原本的从txt文本导入,我做成了从音乐文件(*.mp3 *.wav *.ape *.flac)导入。在导入过程中,需要手动输入拼音缩写。 顺带提一下,我听的歌名字都是日文/英文,拼音缩写就很zz了。 在实现过程中,我并不想用除Qt以外其他的库,因为找库和安装感觉有点麻烦。但是麻烦这点终究逃不过。 首先添加 multimedia 模块,在 .pro项目文件中加上: QT += core gui QT += multimedia greaterThan(QT_MAJOR_VERSION, 4): QT += widgets (写这篇blog的时候已经忘了这几个语句的作用了) QMediaPlayer *player = new QMediaPlayer; player->setMedia(some_url); player->play(); 按理上面3行代码就能播放音乐了,但是,什么声音都没有而且程序输出了一串神秘的错误信息。 一番折腾后,我知道了,还需要自行下载安装解码器(如LAV Filters) 听到程序放出了歌,有点小开心。 然后是从文件获取歌曲信息,找来找去只找到下面一种方式: connect(player, &QMediaObject::metaDataChanged, this, &getMetaData);//这里简写了 player->setMedia(some_url);
void getMetaData() { ... player->metaData(const QString &key) ... } /* player->setMedia(some_url); player->metaData(const QString &key); 如果这样不使用信号槽的话,无法获取正确的信息 */ 无法直接获取歌曲信息... 一番思索后想出的解决方法: new 一个QMediaPlayer对象,连接信号和槽,获取后delete,然后再new和connect来获取下一首歌曲的信息 int curGetInfoRow; void AddSongsDialog::getInfoFromFile(int beginRow) { if (beginRow >= songs.size()) return; curGetInfoRow = beginRow; getInfoProxyPlayer = new QMediaPlayer; connect(getInfoProxyPlayer, QOverload<>::of(&QMediaObject::metaDataChanged), this, &AddSongsDialog::getMetaData); getInfoProxyPlayer->setMedia(songs.at(curGetInfoRow).url); }
//slot void AddSongsDialog::getMetaData() { Song &song = songs[curGetInfoRow]; QString title = getInfoProxyPlayer->metaData(QMediaMetaData::Title).toString(); if (!title.isEmpty()) song.name = title;
QString author = getInfoProxyPlayer->metaData(QMediaMetaData::Author).toStringList().join(", "); if (!author.isEmpty()) song.singerName = author;
delete getInfoProxyPlayer;
curGetInfoRow++; if (curGetInfoRow >= songs.size()) return;
getInfoProxyPlayer = new QMediaPlayer; connect(getInfoProxyPlayer, QOverload<>::of(&QMediaObject::metaDataChanged), this, &AddSongsDialog::getMetaData); getInfoProxyPlayer->setMedia(songs.at(curGetInfoRow).url); } 歌曲播放控制: 直接参照了Qt示例Music Player 失败的模块文件模块划分: 先做的AdminWindow,后做的UserWindow,带进度条控制的歌曲播放也是做用户窗口时想出来的。当时觉得用户窗口类成员太多,就新开了一个类,但是很多不该放到这个类里的放进去了,然后整体结构就没那么好看。 评分星星: 想实现的是很常见的那种滑动评分的。选择了一张包含所有评分情况的图片(从豆瓣上搞来的),鼠标移动时切换到对应的图片。 classRatingWidegt :public QLabel { Q_OBJECT protected: //鼠标事件 voidmouseMoveEvent(QMouseEvent *ev); voidmouseReleaseEvent(QMouseEvent *ev); voidleaveEvent(QEvent *ev); ...... }
//更新显示调用的主要函数 QLabel::setPixmap(const QPixmap &); QPixmap::copy(int x, int y, int width, int height); 播放列表窗: 想法是点下“列表”按钮,显示播放列表的小窗,然后点击非该窗口的地方关闭窗口。自己想了一个方案: MyWidget *playListWidget; connect(showTableBtn, &QAbstractButton::clicked, this, &UserWindowWidgets::showListTable);
classMyWidget :public QWidget { protected: bool event(QEvent *ev); public: MyWidget(QAbstractButton *btn, QWidget *parent = nullptr); private: /* timer的作用是延迟显示列表按钮setEnabled,否则点击按钮会再次打开该窗口 没有想到其他解决方法,只有这个妥协了的方案 */ QTimer timer; QAbstractButton *btn; };
MyWidget::MyWidget(QAbstractButton *button, QWidget *parent): QWidget(parent), btn(button) { timer.setSingleShot(true); timer.setInterval(50); connect(&timer, &QTimer::timeout, this, [this]{ btn->setEnabled(true); }); setWindowFlag(Qt::Popup); } bool MyWidget::event(QEvent *ev) { if (ev->type() == QEvent::Hide) timer.start(); if (ev->type() == QEvent::ActivationChange) if (QApplication::activeWindow() != this) hide(); return QWidget::event(ev); }
void UserWindowWidgets::showListTable() { showTableBtn->setEnabled(false); //点的数据是大致计算然后多次测试得来的 QPoint p = showTableBtn->mapToGlobal(QPoint(0,0)) - QPoint(316, 240); playListWidget->move(p); playListWidget->show(); } ![]() 图标哪来的? 在电脑上下载app.apk文件,解压缩,翻文件夹。 这里所有图标均来自于网易云音乐app. 参考Qt官方示例:
文件下载:
(别说github,又不是真公开,不想麻烦再创个号) 这个项目是在3月底到4月初做的,到写这篇blog已经过了3个多月,再翻代码还是基本都看得懂的,虽然什么注释都没写。 也算是真正写了一个项目,不过和实际项目工程相比还是太幼稚太年轻。 第四周的最终展示,应该是我第一次自己做出成果然后pre的了,讲的挺差的,不过一运行程序还是挺惊艳的,有了“产品”的标签。 ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:VoidZero,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |