【Qt C++自绘制界面音乐播放器-7】内容展示区--声音素材展示

45 阅读7分钟

最新大型开源项目-云游戏,云桌面系统,欢迎关注

GammaRay源码地址

本项目代码地址

源码

内容区域

包括声音素材展示和播放控制,其中声音素材展示是相对来说是最复杂的,因为整个声音素材展示的区域,是使用要给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)【水平间距】,【前面元素个数】即为当前元素的索引
  2. 橙色括号,每个元素距离上边的大小为:【上面元素个数】【元素大小】+(【上面元素个数】+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);
...
}