最新大型开源项目-云游戏,云桌面系统,欢迎关注
本项目代码地址
内容区域
包括声音素材展示和播放控制,其中声音素材展示是相对来说是最复杂的,因为整个声音素材展示的区域,是使用要给QWidget绘制的。使用一个QWidget绘制而不是用QListWidget控件的原因是,QListWidget不好控制横向间距,尤其是横向等间距排列。
1.声音素材展示中,每个Item的信息
我们声明一个类,来保存绘制一个Item需要的信息
class AudioItem
{
public:
// 静态方法,用于构造AudioItem,推荐使用这种方式构造一个类
static std::shared_ptr<AudioItem> Make(const QString& icon_url, const QString& name);
// icon_url指的是图片的路径,是Qt资源的路径,例如 :/images/resources/avatar.png
AudioItem(const QString& icon_url, const QString& name);
public:
// 名字,例如原型图中的 海浪
QString name_;
// !!! 绘制的区域,用来计算与鼠标的交互!!!
// 因为一个QWidget绘制了多个Item,所以单独计算
QRect draw_rect_;
// 图片内容
QPixmap pixmap_;
};
std::shared_ptr<AudioItem> AudioItem::Make(const QString& icon_url, const QString& name) {
return std::make_shared<AudioItem>(icon_url, name);
}
AudioItem::AudioItem(const QString& icon_url, const QString& name) {
this->name_ = name;
// 缩放到一个合适的比例
QImage image;
image.load(icon_url);
pixmap_ = QPixmap::fromImage(image);
pixmap_ = pixmap_.scaled(98, 98, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
2.绘制内容展示区
这部分相对来说需要计算的比较多,要细心好好算一下。
1.红色箭头为整个控件,向下偏移的距离
2.绿色箭头是每行之间偏移的距离
3.蓝色箭头是同一行内,每个元素之间,以及元素距离左右边的距离,都是以相同的。
4.从红色箭头指向的绿色边框区域来看,每个元素内部,也需要居中对齐。
我们实现一个ContentPage类,绘制和鼠标事件的处理函数,已经多次使用,不再赘述。
class AudioItem;
class ContentPage : public QWidget
{
Q_OBJECT
public:
// item_count 每行有几个元素
// item_size 每个元素的大小,因为绘制一个元素区域为正方形,所以使用一个代替宽和高
// padding_top 上面提到的,行与行间距的大小,绿色箭头所示
ContentPage(int item_count, int item_size, int padding_top, QWidget *parent = nullptr);
// 绘制
void paintEvent(QPaintEvent *event) override;
// 设置总共有多少个元素,以及元素的名字,元素的图片资源
void UpdateAudioItems(const std::vector<std::shared_ptr<AudioItem>>& items);
// 外边距的大小,红色箭头所示
void SetMarginTop(int mt);
// 鼠标事件
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
// 保存相应的值
int item_count_ = 0;
int item_size_ = 0;
int padding_top_ = 0;
int margin_top_ = 0;
// 当前鼠标的位置
QPoint current_mouse_pos_{-1,-1};
// 当前点击了哪个元素
std::shared_ptr<AudioItem> current_item_ = nullptr;
// 所有的元素数据
std::vector<std::shared_ptr<AudioItem>> audio_items_;
};
首先,我们实现构造函数和几个事件
ContentPage::ContentPage(int item_count, int item_size, int padding_top, QWidget *parent)
: QWidget(parent) {
this->item_count_ = item_count;
this->item_size_ = item_size;
this->padding_top_ = padding_top;
// 这个函数的意思是鼠标不按下的时候,也能产生事件
setMouseTracking(true);
}
// 当更新元素的时候,我们计算一下控件将占用的高度
void ContentPage::UpdateAudioItems(const std::vector<std::shared_ptr<AudioItem>>& items) {
this->audio_items_ = items;
// 首先计算有多少行,比如共12个,每行4个,就是3行
int line_count = items.size() / this->item_count_;
// 计算总的高度
// 上面的外边距 + 下面的外边距 + 行数*(元素大小+行间距)
int total_height = margin_top_ + line_count*(this->item_size_ + this->padding_top_) + this->margin_top_;// another margin_top_ for bottom...
// 设置一下高度
setFixedHeight(total_height);
repaint();
}
void ContentPage::SetMarginTop(int mt) {
this->margin_top_ = mt;
}
void ContentPage::mousePressEvent(QMouseEvent *event) {
// 鼠标按下,获取鼠标位置
current_mouse_pos_ = event->pos();
repaint();
}
void ContentPage::mouseMoveEvent(QMouseEvent *event) {
// 鼠标移动,获取鼠标位置
// 注意,一定要设置 setMouseTracking(true);
current_mouse_pos_ = event->pos();
repaint();
}
void ContentPage::mouseReleaseEvent(QMouseEvent *event) {
repaint();
}
接下来,是最重要的绘制了
- 绿色括号,每个元素距离左边的大小为:【前面元素个数】【元素大小】+(【前面元素个数】+1)【水平间距】,【前面元素个数】即为当前元素的索引
- 橙色括号,每个元素距离上边的大小为:【上面元素个数】【元素大小】+(【上面元素个数】+1)【行间距】,【上面元素个数】即为当前的行数
我们称一个这样的组合为一个元素,每个元素的尺寸计算
void ContentPage::paintEvent(QPaintEvent *event) {
QPainter painter(this);
painter.setRenderHint(QPainter::RenderHint::Antialiasing);
painter.setBrush(QBrush(0xeeeeee));
painter.setPen(Qt::NoPen);
painter.drawRect(this->rect());
// 1. 计算同一行的元素之间,以及元素距离左右边距的大小。
// 如上图所示蓝色箭头,一行4个元素,则有5个边距。
int h_gap = (this->width() - this->item_size_*this->item_count_) / (this->item_count_+1);
for (int audio_idx = 0; audio_idx < this->audio_items_.size(); audio_idx++) {
auto& item = audio_items_[audio_idx];
// 计算同一行的每个元素的索引
// 比如一行3个元素,第0,1,2个元素索引为别是0,1,2,第3个索引则重新变为0
// 0 % 3 => 0
// 1 % 3 => 1
// 2 % 3 => 2
// 3 % 3 => 0
int draw_idx = audio_idx % this->item_count_;
// 计算现在是第几行,第0,1,2个元素索引为别是0,0,0,第3个索引则重新变为1
// 0 / 3 => 0
// 1 / 3 => 0
// 2 / 3 => 0
// 3 / 3 => 1
int line_idx = audio_idx / this->item_count_;
// 按照上面的公式,计算当前元素的左上角坐标
QRect item_rect(draw_idx*this->item_size_ + h_gap*(draw_idx+1),
(this->item_size_)*line_idx + padding_top_*(line_idx+1),
item_size_, item_size_);
// !!! 注意,我们将当前的区域赋值给相应的Item,这样就可以计算鼠标是否与这个Item有交互了。
item->draw_rect_ = item_rect;
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(0xddddee));
// 当鼠标处于这个区域时,我们绘制一个背景
if (item->draw_rect_.contains(current_mouse_pos_)) {
painter.drawRoundedRect(item_rect, 10, 10);
// 记录当前的item
current_item_ = item;
}
// 2. 绘制Item的内容,一个在Item内居中的圆形图案和一个居中的文字
// 将圆形的直径设置为元素宽度的0.7倍,然后计算出左边距
// 上边距我们填写一个默认值,为7
int circle_diameter = item_rect.width()* 0.7;
int left_padding = (item_rect.width() - circle_diameter)/2;
// 将painter此时的状态保存一下,然后绘制完圆形图案后,再重新恢复
painter.save();
// 使用一个圆形最为剪切的路径,将正方形的图片裁剪成圆形
QPainterPath path;
// 路径的起始点要从每个元素的左上角开始计算,然后再加上元素内的偏移量
int circle_x_offset = item_rect.x() + left_padding;
int circle_y_offset = item_rect.y() + 7;
// 元素的整体大小所在的矩形
QRect circle_path_rect(circle_x_offset, circle_y_offset, circle_diameter, circle_diameter);
// 使用圆角矩形来构造圆形
path.addRoundedRect(circle_path_rect, circle_diameter/2, circle_diameter/2);
// 设置裁剪的路径
painter.setClipPath(path);
// 绘制图片,图片将被裁剪成圆形绘制
painter.drawPixmap(item_rect.x() + left_padding, item_rect.y() + 7, item->pixmap_);
// 恢复painter的状态
painter.restore();
// 3. 绘制文字
// 这一步就比较简单了,将文字居中绘制到一个矩形里即可
QPen pen;
QFont font("Microsoft YaHei", 13, QFont::Normal, false);
painter.setFont(font);
painter.setPen(pen);
// 设置文字所在的矩形的高度为33
int font_region_height = 33;
// 注意,第二个参数的意义是,从元素底端,向上偏移33像素。
// item_rect.y() + item_size_ - font_region_height 其中item_rect.y() + item_size就是元素的最底端。
QRect font_rect(item_rect.x(), item_rect.y() + item_size_ - font_region_height, item_size_, font_region_height);
// 设置好绘制参数即可
painter.drawText(font_rect, Qt::AlignHCenter|Qt::AlignVCenter, item->name_);
}
}
3.添加到MainWindow中
因为内容元素可能比较多,所以我们使用一个QScrollArea来存放之前的ContentPage,这样数据多了就可以滑动了。
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent) {
...
// 使用一个垂直的Layout来存储QScrollArea,因为下面还有一个播放控制区。
auto right_content_layout = new QVBoxLayout();
LayoutHelper::ClearMarginSpacing(right_content_layout);
content_area_ = new QScrollArea();
right_content_layout->addWidget(content_area_);
right_content_layout->addStretch();
content_area_->setFixedHeight(400);
content_area_->setStyleSheet("background-color:#ffffff; border:none;");
// 生成一个ContentPage,并添加到QScrollArea中去
content_page_ = new ContentPage(4, 140, 40, this);
int real_content_width = Settings::kWindowWidth - Settings::kSideBarWidth - 5;
content_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
content_area_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
content_area_->setWidget(content_page_);
content_page_->setFixedWidth(real_content_width);
content_page_->SetMarginTop(10);
// 构造几个测试数据,并更新到ContentPage中
{
std::vector<std::shared_ptr<AudioItem>> items;
items.push_back(AudioItem::Make(":/images/resources/preset_1.jpg", "海浪之声"));
items.push_back(AudioItem::Make(":/images/resources/preset_2.jpg", "风和日丽"));
...
content_page_->UpdateAudioItems(items);
}
...
auto content_layout = new QHBoxLayout();
content_layout->setSpacing(0);
content_layout->setMargin(0);
content_layout->addSpacing(5);
content_layout->addWidget(side_bar_);
// 将整个右面的垂直布局,与之前的SideBar放在一起,左右布局,这样就可以了
content_layout->addLayout(right_content_layout);
...
}