Qt Creator源码分析系列——UI界面:IntroductionWidget类分析和WelcomeMode类初步介绍

该篇文章内容主要集中Qt Creator软件欢迎界面部分代码的分析。从分析插件中的welcome模块开始,项目文件在路径\qt-creator-master\qt-creator-master\src\plugins\welcome下。

分析introductionwidget.cpp/.h文件

首先说明IntroductionWidget类实现了UI Tour,也就是Qt Creator软件初次安装时的用户引导界面,同样可以在工具栏中的帮助中找到UI Tour。
在introductionwidget.h文件中定义了一个结构体Item和一个类IntroductionWidget。
结构体由4个QString类型的变量组成,分别为pointerAnchorObjectName、title、brief、description。

struct Item
{
    QString pointerAnchorObjectName;
    QString title;
    QString brief;
    QString description;
};

下面是这个Item的一个例子,title指定图片中的字体较大的标题,比如这里的Mode Selector。brief就是下一行简要介绍标题的含义。依次类推,可以解释Item结构体的作用。
在这里插入图片描述
在这里插入图片描述

IntroductionWidget类继承自QWidget。QPointer类是模板类,它提供指向QObject的受保护的指针。

class IntroductionWidget : public QWidget
{
    Q_OBJECT
public:
    explicit IntroductionWidget(QWidget *parent = nullptr);
    static void askUserAboutIntroduction(QWidget *parent, QSettings *settings);

protected:
    bool event(QEvent *e) override;
    bool eventFilter(QObject *obj, QEvent *ev) override;
    void paintEvent(QPaintEvent *ev) override;
    void keyPressEvent(QKeyEvent *ke) override;
    void mouseReleaseEvent(QMouseEvent *me) override;
private:
    void finish();
    void step();
    void setStep(uint index);
    void resizeToParent();

    QWidget *m_textWidget;  // 引导页面QWidget
    QLabel *m_stepText;  // 详解文本内容
    QLabel *m_continueLabel;  // 文本内容
    QImage m_borderImage;
    QString m_bodyCss;
    std::vector<Item> m_items;  // UI Tour中详解文字内容
    QPointer<QWidget> m_stepPointerAnchor;  //引导中箭头需要指向的QWidget
    uint m_step = 0;
};

首先IntroductionWidget类本身就是一个QWidget,其内部包含着引导页面的成员变量QWidget的指针:m_textWidget。

构造函数

在这里插入图片描述
从构造函数看起,构造列表上直接使用m_borderImage(":/welcome/images/border.png")初始化QImage类型的成员,也就是下面的图片。
在这里插入图片描述
focusPolicy是QWidget类的属性,它的类型是Qt::FocusPolicy。此属性保存widget部件接受键盘焦点的方式。如果窗口widget部件通过制表键接受键盘焦点,则策略为Qt :: TabFocus;如果窗口widget部件通过单击接受焦点,则策略为Qt :: ClickFocus;如果窗口widget部件同时接受两者,则策略为Qt :: StrongFocus;如果不接受,则策略为Qt :: NoFocus(默认)。
如果widget部件处理键盘事件,则必须为其启用键盘焦点。通常这是通过widget部件的构造函数完成。 例如,QLineEdit构造函数调用setFocusPolicy(Qt :: StrongFocus)。如果小部件具有焦点代理,则焦点策略将传播给它。
在这里插入图片描述
setFocus是一个重载功能。如果此窗口widget部件或其父项之一是活动窗口,则将键盘输入焦点赋予该窗口小部件(或其焦点代理)。
在这里插入图片描述
事件过滤器是一个对象,它接收发送到该对象的所有事件。 筛选器可以停止事件,也可以将其转发到该对象。 事件过滤器filterObj通过其eventFilter()函数接收事件。 如果应该过滤事件(即停止),则eventFilter()函数必须返回true; 否则必须返回false。
在这里插入图片描述
新建QWidget对象,将指针赋给m_textWidget成员,新建垂直布局QVBoxLayout,作为QWidget对象的布局。新建QLabel用于显示上述UI Tour需要介绍的文字内容。新建QLabel,将指针赋给m_continueLabel,用于显示文本“UI Introduction */10”。

m_textWidget = new QWidget(this);
auto layout = new QVBoxLayout;
m_textWidget->setLayout(layout);
m_stepText = new QLabel(this);
m_stepText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_stepText->setWordWrap(true);
m_stepText->setTextFormat(Qt::RichText);
// why is palette not inherited???
m_stepText->setPalette(palette());
m_stepText->setOpenExternalLinks(true);
m_stepText->installEventFilter(this);
layout->addWidget(m_stepText);

m_continueLabel = new QLabel(this);
m_continueLabel->setAlignment(Qt::AlignCenter);
m_continueLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_continueLabel->setWordWrap(true);
auto fnt = font();
fnt.setPointSizeF(fnt.pointSizeF() * 1.5);
m_continueLabel->setFont(fnt);
m_continueLabel->setPalette(palette());
layout->addWidget(m_continueLabel);

openExternalLinks是QLabel类的属性,类型是bool。指定QLabel是否应该使用QDesktopServices :: openUrl()自动打开链接,而不是发出linkActivated()信号。
在这里插入图片描述
在这里插入图片描述
设置QString类型的成员m_bodyCss。填充vector类型的m_items。
在这里插入图片描述
setStep用于设置引导到第几步。
在这里插入图片描述
setStep用于m_step、m_continueLabel和m_stepText指向标签的内容。对于设置m_stepPointerAnchor:如果存放显示内容的m_items中第m_step的pointerAnchorObjectName为空,m_stepPointerAnchor也清空。如果不为空,通过anchorObjectName找到兄弟widget。

void IntroductionWidget::setStep(uint index)
{
    QTC_ASSERT(index < m_items.size(), return);
    m_step = index;
    m_continueLabel->setText(tr("UI Introduction %1/%2 >").arg(m_step + 1).arg(m_items.size()));
    const Item &item = m_items.at(m_step);
    m_stepText->setText("<html><body style=\"" + m_bodyCss + "\">" + "<h1>" + item.title
                        + "</h1><p>" + item.brief + "</p>" + item.description + "</body></html>");
    const QString anchorObjectName = m_items.at(m_step).pointerAnchorObjectName;
    if (!anchorObjectName.isEmpty()) {
        m_stepPointerAnchor = parentWidget()->findChild<QWidget *>(anchorObjectName);
        QTC_CHECK(m_stepPointerAnchor);
    } else {
        m_stepPointerAnchor.clear();
    }
    update();
}

在这里插入图片描述
parentWidget函数返回目前widget的父级,如果没有父widget则返回0。
在这里插入图片描述
返回此对象的子代,该子代可以转换为T类型,称为name;如果没有此类对象,则返回0。 省略name参数会使所有对象名称匹配。 除非选项指定选项FindDirectChildrenOnly,否则将以递归方式执行搜索。如果有不止一个孩子符合搜索条件,则返回最直接的祖先。 如果有多个直接祖先,则不确定将返回哪一个。

设置IntroductionWidget的Geometry大小,并设置引导页面的Geometry大小。

void IntroductionWidget::resizeToParent()
{
    QTC_ASSERT(parentWidget(), return);
    setGeometry(QRect(QPoint(0, 0), parentWidget()->size()));
    m_textWidget->setGeometry(QRect(width()/4, height()/4, width()/2, height()/2));
}

事件过滤器

bool IntroductionWidget::eventFilter(QObject *obj, QEvent *ev)
{
    if (obj == parent() && ev->type() == QEvent::Resize)
        resizeToParent();
    else if (obj == m_stepText && ev->type() == QEvent::MouseButtonRelease)
        step();
    return QWidget::eventFilter(obj, ev);
}

第一个条件判断是指明获取到IntroductionWidget父级的大小改变事件,IntroductionWidget也需要跟随父级改变。第二个判断是m_stepText获取到了鼠标按钮释放事件,则调用step函数。

事件处理

拦截部分事件:QEvent::ShortcutOverride

扫描二维码关注公众号,回复: 10148332 查看本文章
bool IntroductionWidget::event(QEvent *e)
{
    if (e->type() == QEvent::ShortcutOverride) {
        e->accept();
        return true;
    }
    return QWidget::event(e);
}

在这里插入图片描述
按键按下事件处理,当按下esc时,直接退出引导。如果没有按下键盘修饰符,则进行下一步处理。根据应用布局方向,对left和right键进行特殊处理。
在这里插入图片描述
对于鼠标按下事件的处理,则是直接accpet截获,然后进行界面切换step。

void IntroductionWidget::keyPressEvent(QKeyEvent *ke)
{
    if (ke->key() == Qt::Key_Escape)
        finish();
    else if ((ke->modifiers()
              & (Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier))
             == Qt::NoModifier) {
        const Qt::Key backKey = QGuiApplication::isLeftToRight() ? Qt::Key_Left : Qt::Key_Right;
        if (ke->key() == backKey) {
            if (m_step > 0)
                setStep(m_step - 1);
        } else {
            step();
        }
    }
}

void IntroductionWidget::mouseReleaseEvent(QMouseEvent *me)
{
    me->accept();
    step();
}

这里涉及到两个函数step和finsh。其中hide是QWidget的槽函数,用于隐藏QWidget界面。deleteLater是QObject的槽函数。

void IntroductionWidget::finish()
{
    hide();
    deleteLater();
}

在这里插入图片描述
计划删除该对象。当控制权返回事件循环时,该对象将被删除。 如果调用此函数时事件循环未运行(例如,在QCoreApplication :: exec()之前在对象上调用deleteLater()),则一旦事件循环启动,该对象将被删除。 如果在主事件循环停止后调用deleteLater(),则不会删除该对象。 从Qt 4.8开始,如果在没有运行事件循环的线程中的对象上调用deleteLater(),则该对象在线程完成时将被销毁。
请注意,进入和离开新的事件循环(例如,通过打开模式对话框)不会执行延迟删除; 对于要删除的对象,控件必须返回到调用deleteLater()的事件循环。
注意:多次调用此函数是安全的; 传递第一个延迟的删除事件时,将从事件队列中删除该对象的所有未决事件。

step函数作用用于切换页面

void IntroductionWidget::step()
{
    if (m_step >= m_items.size() - 1)
        finish();
    else
        setStep(m_step + 1);
}

下面我们看最重要的paintEvent事件,这里先看主要逻辑。如果m_stepPointerAnchor是空指针,则不需要绘制绿色的箭头和高亮区域,仅仅需要绘制背景。

void IntroductionWidget::paintEvent(QPaintEvent *)
{
	QPainter p(this);
    p.setOpacity(.87);
    const QColor backgroundColor = Qt::black;
    if (m_stepPointerAnchor) {
    	...
    } else {
		p.fillRect(rect(), backgroundColor);
	}
}

在这里插入图片描述
用指定的画笔填充给定的矩形。另外,您可以指定QColor而不是QBrush,QBrush构造函数(带有QColor参数)将自动创建一个实心图案画笔。
下面来看一下需要绘制箭头和高亮区域的代码片段:

const QPoint anchorPos = m_stepPointerAnchor->mapTo(parentWidget(), {0, 0});
const QRect anchorRect(anchorPos, m_stepPointerAnchor->size());
const QRect spotlightRect = anchorRect.adjusted(-SPOTLIGHTMARGIN, -SPOTLIGHTMARGIN, SPOTLIGHTMARGIN,                    SPOTLIGHTMARGIN);

// darken the background to create a spotlighted area
// 将其他区域设置为背景色从而创建高亮区域
if (spotlightRect.left() > 0) {
	// 将需要高亮区域的左侧设置为背景色
	p.fillRect(0, 0, spotlightRect.left(), height(), backgroundColor);
}
if (spotlightRect.top() > 0) {
    // 将需要高亮区域的上侧设置为背景色
    p.fillRect(spotlightRect.left(), 0, width() - spotlightRect.left(), spotlightRect.top(), backgroundColor);
}
if (spotlightRect.right() < width() - 1) {
    // 将需要高亮区域的右侧设置为背景色
	p.fillRect(spotlightRect.right() + 1, spotlightRect.top(), width() - spotlightRect.right() - 1, height() - spotlightRect.top(), backgroundColor);
}
if (spotlightRect.bottom() < height() - 1) {
    // 将需要高亮区域的下侧设置为背景色
    p.fillRect(spotlightRect.left(), spotlightRect.bottom() + 1, spotlightRect.width(), height() - spotlightRect.bottom() - 1, backgroundColor);
}

// smooth borders of the spotlighted area by gradients
// 高亮区域边缘使用渐变色,这个类是自定义的,后续章节再详细介绍
StyleHelper::drawCornerImage(m_borderImage, &p, spotlightRect, SPOTLIGHTMARGIN, SPOTLIGHTMARGIN, SPOTLIGHTMARGIN, SPOTLIGHTMARGIN);

// draw pointer 画箭头
const QColor qtGreen(65, 205, 82);
p.setOpacity(1.);
p.setPen(QPen(QBrush(qtGreen), POINTER_WIDTH, Qt::SolidLine, Qt::RoundCap, Qt::MiterJoin));
p.setRenderHint(QPainter::Antialiasing);
for (const QPolygonF &poly : pointerPolygon(spotlightRect, rect()))
	p.drawPolyline(poly);

将所指定的坐标值转换为父级相应的坐标值const QPoint anchorPos = m_stepPointerAnchor->mapTo(parentWidget(), {0, 0})
在这里插入图片描述
将widget部件坐标系中的pos转换为父级坐标系中相应点的pos值。父级不能为0,并且必须是调用的widget部件的父级。

这里创建的QRect就是需要高亮的子部件,代码如const QRect anchorRect(anchorPos, m_stepPointerAnchor->size())。并在下一行代码中对QRect进行微调,赋值给QRect类型变量spotlightRect。

使用QPolygonF提供的点的坐标集合,利用drawPolyline函数画多边形。
在这里插入图片描述

pointerPolygon返回装有QPolygonF类型的QVector容器。

static const QVector<QPolygonF> pointerPolygon(const QRect &anchorRect, const QRect &fullRect)
{
    // Put the arrow opposite to the smallest margin,
    // with priority right, top, bottom, left.
    // Not very sophisticated but sufficient for current use cases.
    QVector<Qt::Alignment> alignments{Qt::AlignRight, Qt::AlignTop, Qt::AlignBottom, Qt::AlignLeft};
    Utils::sort(alignments, [anchorRect, fullRect](Qt::Alignment a, Qt::Alignment b) {
        return oppositeMargin(anchorRect, fullRect, a) < oppositeMargin(anchorRect, fullRect, b);
    });
    const auto alignIt = std::find_if(alignments.cbegin(),
                                      alignments.cend(),
                                      [anchorRect, fullRect](Qt::Alignment align) {
                                          return margin(anchorRect, fullRect, align) > POINTER_SIZE;
                                      });
    if (alignIt == alignments.cend())
        return {{}}; // no side with sufficient space found
    const qreal arrowHeadWidth = POINTER_SIZE/3.;
    if (*alignIt == Qt::AlignRight) {
        const qreal middleY = anchorRect.center().y();
        const qreal startX = anchorRect.right() + POINTER_SIZE;
        const qreal endX = anchorRect.right() + POINTER_WIDTH;
        return {{QVector<QPointF>{{startX, middleY}, {endX, middleY}}},
                QVector<QPointF>{{endX + arrowHeadWidth, middleY - arrowHeadWidth},
                                 {endX, middleY},
                                 {endX + arrowHeadWidth, middleY + arrowHeadWidth}}};
    }
    if (*alignIt == Qt::AlignTop) {
        const qreal middleX = anchorRect.center().x();
        const qreal startY = anchorRect.y() - POINTER_SIZE;
        const qreal endY = anchorRect.y() - POINTER_WIDTH;
        return {{QVector<QPointF>{{middleX, startY}, {middleX, endY}}},
                QVector<QPointF>{{middleX - arrowHeadWidth, endY - arrowHeadWidth},
                                 {middleX, endY},
                                 {middleX + arrowHeadWidth, endY - arrowHeadWidth}}};
    }
    if (*alignIt == Qt::AlignBottom) {
        const qreal middleX = anchorRect.center().x();
        const qreal startY = anchorRect.y() + POINTER_WIDTH;
        const qreal endY = anchorRect.y() + POINTER_SIZE;
        return {{QVector<QPointF>{{middleX, startY}, {middleX, endY}}},
                QVector<QPointF>{{middleX - arrowHeadWidth, endY + arrowHeadWidth},
                                 {middleX, endY},
                                 {middleX + arrowHeadWidth, endY + arrowHeadWidth}}};
    }

    // Qt::AlignLeft
    const qreal middleY = anchorRect.center().y();
    const qreal startX = anchorRect.x() - POINTER_WIDTH;
    const qreal endX = anchorRect.x() - POINTER_SIZE;
    return {{QVector<QPointF>{{startX, middleY}, {endX, middleY}}},
            QVector<QPointF>{{endX - arrowHeadWidth, middleY - arrowHeadWidth},
                             {endX, middleY},
                             {endX - arrowHeadWidth, middleY + arrowHeadWidth}}};
}

分析welcomeplugin.cpp文件

WelcomeMode类

WelcomeMode类继承自IMode类,IMode类定义在src/plugins/coreplugin/Headers/progrssmanager\imode.h中。而IMode类继承自IContext,IContext继承自QObject。这里不对父类进行探究,只对WelcomeMode类进行分析。

class WelcomeMode : public IMode
{
    Q_OBJECT
public:
    WelcomeMode();
    ~WelcomeMode();
    void initPlugins();
    Q_INVOKABLE bool openDroppedFiles(const QList<QUrl> &urls);
private:
    void addPage(IWelcomePage *page);
    QWidget *m_modeWidget;
    QStackedWidget *m_pageStack;
    SideBar *m_sideBar;
    QList<IWelcomePage *> m_pluginList;
    QList<WelcomePageButton *> m_pageButtons;
    Id m_activePage;
};

WelcomeMode类包含了指向QWidget的指针m_modeWidget、指向QStackedWidget的指针m_pageStack(QStackedWidget类提供了一堆widget部件,其中一次仅一个小部件可见)、上一篇解析的指向SiderBar的指针m_sideBar、包含IWelcomePage指针的QList容器m_pluginList、包含WelcomePageButton指针的QList容器m_pageButtons以及Id类m_activePage。

先看该函数的构造函数,只看和显示相关部分代码

//和IMode类有关,现在暂不讲解
setDisplayName(tr("Welcome"));
const Icon CLASSIC(":/welcome/images/mode_welcome.png");
const Icon FLAT({{":/welcome/images/mode_welcome_mask.png", Theme::IconsBaseColor}});
const Icon FLAT_ACTIVE({{":/welcome/images/mode_welcome_mask.png",
                             Theme::IconsModeWelcomeActiveColor}});
setIcon(Icon::modeIcon(CLASSIC, FLAT, FLAT_ACTIVE));
setPriority(Constants::P_MODE_WELCOME);
setId(Constants::MODE_WELCOME);
setContextHelp("Qt Creator Manual");
setContext(Context(Constants::C_WELCOME_MODE));

QPalette palette = creatorTheme()->palette();
palette.setColor(QPalette::Window, themeColor(Theme::Welcome_BackgroundColor));

m_modeWidget = new QWidget;
m_modeWidget->setPalette(palette);
m_sideBar = new SideBar(m_modeWidget);
auto scrollableSideBar = new QScrollArea(m_modeWidget);
scrollableSideBar->setWidget(m_sideBar);
scrollableSideBar->setWidgetResizable(true);
scrollableSideBar->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollableSideBar->setFrameShape(QFrame::NoFrame);
auto divider = new QWidget(m_modeWidget);
divider->setMaximumWidth(1);
divider->setMinimumWidth(1);
divider->setAutoFillBackground(true);
divider->setPalette(themeColor(Theme::Welcome_DividerColor));
m_pageStack = new QStackedWidget(m_modeWidget);
m_pageStack->setObjectName("WelcomeScreenStackedWidget");
m_pageStack->setAutoFillBackground(true);
auto hbox = new QHBoxLayout;
hbox->addWidget(scrollableSideBar);
hbox->addWidget(divider);
hbox->addWidget(m_pageStack);
hbox->setStretchFactor(m_pageStack, 10);
auto layout = new QVBoxLayout(m_modeWidget);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(new StyledBar(m_modeWidget));
layout->addItem(hbox);
setWidget(m_modeWidget);

为m_modeWidget、m_sideBar和m_pageStack新建相应的对象。新建一个scrollableSideBar对象,将m_sideBar作为它的子级,从下图可以看出滚动条。下一步新建一个水平布局QHBoxLayout,并将scrollableSideBar,divider以及m_pageStack加入。最后新建垂直布局,加入StyledBar(类似工具栏),最后加入上述的水平布局。

在这里插入图片描述
设置滚动区域的widget部件。该部件将成为滚动区域的子级,并且在删除滚动区域或设置新的小部件时将被销毁。

在这里插入图片描述在这里插入图片描述
WelcomeMode类的其他方法与插件系统有关,后面继续介绍。

发布了134 篇原创文章 · 获赞 141 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/asmartkiller/article/details/104301928