简介
前问提到了PicAnimationWid,PicButton,PreListWid等类,这里介绍如何实现这些类,并串联起来达到幻灯片放映的效果。
SlideShowDlg
在左侧目录树右击时弹出菜单,新增幻灯片播放的选项,点击后会弹出SlideShowDlg。SlideShowDlg的构造函数如下
SlideShowDlg::SlideShowDlg(QWidget *parent, QTreeWidgetItem *first_item,
QTreeWidgetItem *last_item): QDialog(parent),
ui(new Ui::SlideShowDlg),_first_item(first_item),_last_item(last_item)
{
ui->setupUi(this);
}
_first_item表示播放的第一个item _last_item表示播放的最后一个item 先将slideshow展示出来看看效果,所以在ProTreeWidget的构造函数里添加动作
_action_slideshow = new QAction(QIcon(":/icon/slideshow.png"), tr("轮播图播放"),this);
connect(_action_slideshow, &QAction::triggered, this, &ProTreeWidget::SlotSlideShow);
在点击槽函数里完善右键点击逻辑
void ProTreeWidget::SlotItemPressed(QTreeWidgetItem *pressedItem, int column)
{
qDebug() << "ProTreeWidget::SlotItemPressed" << endl;
if(QGuiApplication::mouseButtons() == Qt::RightButton) //判断是否为右键
{
QMenu menu(this);
qDebug() << "menu addr is " << &menu << endl;
int itemtype = (int)(pressedItem->type());
if (itemtype == TreeItemPro)
{
_right_btn_item = pressedItem;
menu.addAction(_action_import);
menu.addAction(_action_setstart);
menu.addAction(_action_closepro);
menu.addAction(_action_slideshow);
menu.exec(QCursor::pos()); //菜单弹出位置为鼠标点击位置
}
}
}
实现槽函数SlotSlideShow,n内部创建一个SlideShowDlg智能指针对象,然后设置为模态对话框,并且最大化显示。
void ProTreeWidget::SlotSlideShow(){
if(!_right_btn_item){
return;
}
auto *right_pro_item = dynamic_cast<ProTreeItem*>(_right_btn_item);
auto * last_child_item = right_pro_item->GetLastPicChild();
if(!last_child_item){
return;
}
qDebug()<< "last child item name is " << last_child_item->GetPath()<< endl;
auto * first_child_item = right_pro_item->GetFirstPicChild();
if(!first_child_item){
return;
}
qDebug()<< "first child item name is " << first_child_item->GetPath()<< endl;
_slide_show_dlg = std::make_shared<SlideShowDlg>(this, first_child_item, last_child_item);
_slide_show_dlg->setModal(true);
_slide_show_dlg->showMaximized();
}
SlotSlideShow里根据right_pro_item获取当前项目下第一个子item和最后一个子item,这个算法并不复杂,right_pro_item为项目的root item,所以可以递归的获取root下第一个非文件夹item节点和最后一个非文件夹item节点。 遍历根目录所有一级子节点,如果第一个一级子节点为文件夹则递归查找,直到找到,如果第一个一级子节点下不存在图片类型的item,那么
ProTreeItem *ProTreeItem::GetFirstPicChild()
{
if(this->type() == TreeItemPic){
return nullptr;
}
auto child_count = this->childCount();
if(child_count == 0){
return nullptr;
}
for(int i = 0; i < child_count-1; i++){
auto * first_child = this->child(i);
auto * first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
auto item_type = first_tree_child->type();
if(item_type == TreeItemPic){
return first_tree_child;
}
first_child = first_tree_child->GetFirstPicChild();
if(!first_child){
continue;
}
first_tree_child = dynamic_cast<ProTreeItem*>(first_child);
return first_tree_child;
}
return nullptr;
}
获取最后一个子节点逻辑类似
ProTreeItem *ProTreeItem::GetLastPicChild()
{
if(this->type() == TreeItemPic){
return nullptr;
}
auto child_count = this->childCount();
if(child_count == 0){
return nullptr;
}
for(int i = child_count-1; i >= 0; i--){
auto* last_child = this->child(i);
auto * last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
int item_type = last_tree_item->type();
if(item_type == TreeItemPic){
return last_tree_item;
}
last_child = last_tree_item->GetLastPicChild();
if(!last_child){
continue;
}
last_tree_item = dynamic_cast<ProTreeItem*>(last_child);
return last_tree_item;
}
return nullptr;
}
此时点击幻灯片播放菜单就会弹出这个对话框了,为了样式美观我们添加qss样式
SlideShowDlg {
color:rgb(231,231,231);
background-color:rgb(46,47,48);
}
PicAnimationWid
接下来我们需要在SlideShowDlg的动画区域添加动画逻辑,类PicAnimationWid为图片动画展示窗口,继承于QWidget,构造函数比较简单
PicAnimationWid::PicAnimationWid(QWidget *parent) : QWidget(parent),_factor(0.0),
_cur_item(nullptr),_b_start(false)
{
_timer = new QTimer(this);
connect(_timer, &QTimer::timeout, this, &PicAnimationWid::TimeOut);
}
- _factor为动画因子,控制图片渐隐效果
- _b_start控制动画是否播放
- _cur_item 表示当前要绘制显示的ProTreeItem对象。
启动了一个定时器,然后定时回调TimeOut函数 同样的道理析构函数需要实现定时器的停止
PicAnimationWid::~PicAnimationWid(){ _timer->stop(); // delete _timer; }
定时器回调函数
void PicAnimationWid::TimeOut() { if(!_cur_item){ Stop(); update(); return; } //qDebug()<<"_factor is " << _factor << endl; _factor = _factor+0.01; if(_factor >= 1){ _factor = 0; auto * cur_pro_item = dynamic_cast<ProTreeItem*>(_cur_item); auto * next_pro_item = cur_pro_item->GetNextItem(); if(!next_pro_item){ Stop(); update(); return; } SetPixmap(next_pro_item); update(); return; } update(); }
该函数每次对factor增加0.01,进而控制动画,如果factor变为1说明已经完成一张图片的消失和另一张的展示,需要更新下一组两张图片用来做渐隐渐现的效果。 update函数是基类的刷新函数,会触发paintEvent函数,这个函数功能之后介绍。先介绍SetPixmap函数,该函数用来加载两张图片做渐变效果。 实现SetPixmap设置要绘制的图片
void PicAnimationWid::SetPixmap(QTreeWidgetItem *item) { if(!item){ return; } auto * tree_item = dynamic_cast<ProTreeItem*>(item); auto path = tree_item->GetPath(); _pixmap1.load(path); _cur_item = tree_item; if(_map_items.find(path) == _map_items.end()){ _map_items[path]=tree_item; qDebug() << "SetPixmap path is " << path << endl; emit SigUpPreList(item); } emit SigSelectItem(item); auto * next_item = tree_item->GetNextItem(); if(!next_item){ return; } auto next_path = next_item->GetPath(); _pixmap2.load(next_path); if(_map_items.find(next_path) == _map_items.end()){ _map_items[next_path] = next_item; emit SigUpPreList(next_item); } }
因为要双缓冲绘图,所以要缓存两张图片,用_pixmap1和_pixmap2缓存。 实现_pixmap1渐隐,_pixmap2渐现。 SigUpPreList信号是用来通知下方预览框更新预览图,因为我们要做的是上方播放动画后,下方会更新预览图。 当前正在播放的图在下方预览图有选中提示,所以SigSelectItem信号是用来通知下方预览图选中效果。 接下来要实现开始函数,让动画动起来
void PicAnimationWid::Start() { emit SigStart(); emit SigStartMusic(); _factor = 0; _timer->start(25); _b_start = true; }
- SigStart信号用来通知右上方按钮的显示播放还是暂停状态,之后在处理信号连接问题。
- _factor为动画因子
- _b_start被设置为true
- 定时器每隔25ms更新一次
- SigStartMusic信号用来更新音乐,之后再处理信号连接问题。
同样实现一个停止动画的逻辑
void PicAnimationWid::Stop()
{
emit SigStop();
emit SigStopMusic();
_timer->stop();
_factor = 0;
_b_start = false;
}
我们接下来要实现双缓冲绘图的逻辑
void PicAnimationWid::paintEvent(QPaintEvent *event)
{
if(_pixmap1.isNull()){
return;
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
QRect rect = geometry();
int w = rect.width();
int h = rect.height();
_pixmap1=_pixmap1.scaled(w,h,Qt::KeepAspectRatio);
int alpha = 255 * (1.0f - _factor);
//qDebug()<<"_pixmap1.size()" << _pixmap1.size() << endl;
QPixmap alphaPixmap(_pixmap1.size());
alphaPixmap.fill(Qt::transparent);
QPainter p1(&alphaPixmap);
p1.setCompositionMode(QPainter::CompositionMode_Source);
p1.drawPixmap(0, 0, _pixmap1);
p1.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p1.fillRect(alphaPixmap.rect(), QColor(0, 0, 0, alpha));
p1.end();
int x = (w - _pixmap1.width()) / 2;
int y = (h - _pixmap1.height()) / 2;
painter.drawPixmap(x, y, alphaPixmap);
if(_pixmap2.isNull()){
return;
}
_pixmap2=_pixmap2.scaled(w,h,Qt::KeepAspectRatio);
alpha = 255 * (_factor);
QPixmap alphaPixmap2(_pixmap2.size());
alphaPixmap2.fill(Qt::transparent);
QPainter p2(&alphaPixmap2);
p2.setCompositionMode(QPainter::CompositionMode_Source);
p2.drawPixmap(0, 0, _pixmap2);
p2.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p2.fillRect(alphaPixmap2.rect(), QColor(0, 0, 0, alpha));
p2.end();
x = (w - _pixmap2.width()) / 2;
y = (h - _pixmap2.height()) / 2;
painter.drawPixmap(x, y, alphaPixmap2);
}
所谓双缓冲绘图逻辑如下:
- 提前加载好图片的两个pixmap分别为_pixmap1和_pixmap2。然后基于现在的widget大小做等比拉伸。
- 创建两个pixmap用作遮盖,分别为alphaPixmap和alphaPixmap2,将他们填充为透明的颜色。
- 分别创建两个画刷,然后绑定alphaPixmap和alphaPixmap2,用画刷分别绘制_pixmap1和_pixmap2。
- CompositionMode_DestinationIn表示遮罩的模式为显示重叠区域,CompositionMode_Source表示原图的绘制模式。
- 最后根据alpha值分别p2和p1的两个矩形区域设置透明度。
- 最后统一用一个painter分别绘制两个alphaPixmap和alphaPixmap2。
我们回到SlideShowDlg的ui文件,将动画显示区的widget升级为PicAnimationWid类即可。 然后在其构造函数里添加对动画区域的调用
ui->picAnimation->SetPixmap(_first_item);
ui->picAnimation->Start();
然后我们运行程序右键目录树的root项目选择幻灯片播放就可以看到动画效果了。
预览图类PreListWid
PreListWid类是从QListWidget派生而来。我们将SlideShowDlg中的preListWidget升级为PreListWid类型。 然后在ui文件中设置其高度为固定的110,因为之前我们设置其父窗口widget高度为固定的120,之后再设置其最大宽度为1677215,一个很大的数,列表不换行。 属性配置如下
构造函数
PreListWid::PreListWid(QWidget *parent):QListWidget(parent),_global(0),_last_index(17)
{
this->setViewMode(QListWidget::IconMode);//设置内容为图片
this->setIconSize(QSize(PREICON_SIZE, PREICON_SIZE));//设置图片的大小
this->setSpacing(5);//设置每个item之间的间隔大小
connect(this,&PreListWid::itemPressed, this, &PreListWid::SlotItemPressed);
}
- 构造函数里设置视图模式为图片模式
- _global为计数器,统计累计加入列表的item数量,为每个item生成计数id。
- _last_index为上一次选择的item的id,因为全屏模式下列表框最多显示17个item,那么初始为17。该变量主要用于控制PreListWid是否横向移动,以及移动多少像素,因为上方展示的图片在下方的预览图可能在屏幕外,所以要移动PreListWid做显示效果。
- 连接了PreListWid的itemPressed信号,所以当item被点击后会触发SlotItemPressed函数。
PreListItem
为了实现我们自己的功能,所以PreListWid的item要自己实现,PreListItem继承于QListWidgetItem类。 其基本功能包括
PreListItem::PreListItem(const QIcon &icon, const QString &text,const int &index,
QListWidget *view , int type ):
QListWidgetItem (icon,"",view,type),_path(text),_index(index)
{
}
int PreListItem::GetIndex()
{
return _index;
}
QString PreListItem::GetPath()
{
return _path;
}
_path表示item代表的文件路径 _index表示item的索引,也就是之前提到的id
动画区域和预览区域联动
为使动画区域和预览区域联动,在SlideShowDlg的构造函数里添加信号和槽函数连接逻辑
auto * prelistWid = dynamic_cast<PreListWid*>(ui->preListWidget);
connect(ui->picAnimation, &PicAnimationWid::SigUpPreList, prelistWid,&PreListWid::SlotUpPreList);
connect(ui->picAnimation, &PicAnimationWid::SigSelectItem, prelistWid, &PreListWid::SlotUpSelect);
//连接下方预览条点击与上方动画区图片显示
connect(prelistWid, &PreListWid::SigUpSelectShow,
ui->picAnimation, &PicAnimationWid::SlotUpSelectShow);
- 连接了PicAnimationWid的SigUpPreList信号,可以实现上面动画播放时将图像的预览图添加到列表中的效果。
- 连接了PicAnimationWid的SigSelectItem信号,可以实现上面动画播放时根据图像显示预览图选中效果。
- 连接了PreListWid的SigUpSelectShow信号,可以实现点击下方预览图,上方显示对应的动画效果。
先实现添加逻辑
void PreListWid::SlotUpPreList(QTreeWidgetItem *tree_item)
{
if(!tree_item){
qDebug() << "tree_item is empty" << endl;
return;
}
auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
auto path = pro_item->GetPath();
auto iter = _set_items.find(path);
if(iter != _set_items.end()){
qDebug() << "path " <<path<< " exists" << endl;
return;
}
AddListItem(path);
}
根据传入的tree_item判断路径是否存在,如果存在则返回,不存在则调用AddListItem将item加入listwidget里。
void PreListWid::AddListItem(const QString &path)
{
QPixmap src_pixmap(path);
src_pixmap = src_pixmap.scaled(PREICON_SIZE,PREICON_SIZE,Qt::KeepAspectRatio);
QPixmap dst_pixmap(QSize(PREICON_SIZE, PREICON_SIZE));
auto src_width = src_pixmap.width();
auto src_height = src_pixmap.height();
auto dist_width = dst_pixmap.width();
auto dist_height = dst_pixmap.height();
dst_pixmap.fill(QColor(220,220,220, 50));
QPainter painter(&dst_pixmap);
auto x = (dist_width-src_width)/2;
auto y = (dist_height-src_height)/2;
painter.drawPixmap(x,y,src_pixmap);
_global++;
PreListItem *pItem = new PreListItem(QIcon(dst_pixmap),path,_global,this);
pItem->setSizeHint(QSize(PREITEM_SIZE,PREITEM_SIZE));
this->addItem(pItem);
_set_items[path] = pItem;
if(_global==1){
_pos_origin = this->pos();
}
}
- 因为图片的大小宽高不同,做拉伸时产生的空白区域不一样,那么我们统一用宽高为PREICON_SIZE的正方形绘制然后填充默认的背景色作为dist_pixmap。
- 然后用一个painter绑定这个dist_pixmap,计算和原图形src_pixmap的差值,让原图形src_pixmap居中绘制在dist_pixmap上。
- 然后构造PreListItem对象,将路径放入集合中。
- 如果_global为1说明时第一张预览图,需要记录一下预览图在其父窗口的位置,这样我们可以根据屏幕最右侧的预览图和该图的位置差值移动PreList Wid。
更新预览图选中效果
void PreListWid::SlotUpSelect(QTreeWidgetItem *tree_item)
{
if(!tree_item){
qDebug() << "tree_item is empty" << endl;
return;
}
auto * pro_item = dynamic_cast<ProTreeItem*>(tree_item);
auto path = pro_item->GetPath();
auto iter = _set_items.find(path);
if(iter == _set_items.end()){
qDebug() << "path " <<path<< " not exists" << endl;
return;
}
auto * list_item = dynamic_cast<PreListItem*>(iter.value());
auto index = list_item->GetIndex();
if(index > 17){
auto pos_cur = this->pos();
this->move(pos_cur.x()-(index-_last_index)*100, pos_cur.y());
_last_index = index;
}else{
this->move(_pos_origin);
_last_index = 17;
}
this->setCurrentItem(iter.value());
}
判断当前播放的图像对应预览图的item的索引是否大于17,如果大于17说明该item在屏幕外,因为一个屏幕最多容纳17个item,所以要移动PreListWid。 移动的方法就是当前索引减去上一次的索引差值乘以100,因为一个item的宽度为100,纵坐标不变。同时设置当前item为选中状态。
点击预览图显示对应图片
PreListWid响应点击的槽函数
void PreListWid::SlotItemPressed(QListWidgetItem *item)
{
if(QGuiApplication::mouseButtons() != Qt::LeftButton){
return;
}
auto * list_item = dynamic_cast<PreListItem*>(item);
auto cur_index = list_item->GetIndex();
auto path = list_item->GetPath();
this->setCurrentItem(item);
emit SigUpSelectShow(path);
}
点击item时发送SigUpSelectShow信号通知动画去显示选择的图片。 触发PicAnimationWid的槽函数SlotUpSelectShow
void PicAnimationWid::SlotUpSelectShow(QString path)
{
qDebug()<<"SlotUpSelectShow path is " << path << endl;
auto iter = _map_items.find(path);
if(iter == _map_items.end()){
return;
}
UpSelectPixmap(iter.value());
update();
}
内部调用UpSelectPixmap更新图片为选中的图片,并且调用update刷新界面。
void PicAnimationWid::UpSelectPixmap(QTreeWidgetItem *item)
{
if(!item){
return;
}
auto * tree_item = dynamic_cast<ProTreeItem*>(item);
auto path = tree_item->GetPath();
_pixmap1.load(path);
_cur_item = tree_item;
if(_map_items.find(path) == _map_items.end()){
_map_items[path]=tree_item;
qDebug() << "SetPixmap path is " << path << endl;
}
auto * next_item = tree_item->GetNextItem();
if(!next_item){
return;
}
auto next_path = next_item->GetPath();
_pixmap2.load(next_path);
if(_map_items.find(next_path) == _map_items.end()){
_map_items[next_path] = next_item;
}
}
到此就实现了幻灯片放映和下方预览图的交互效果。播放和暂停,以及切换操作等留给下一篇。