NEWS

新闻

了解openKylin最新资讯,关注社区和产品动态。

NEWS

Learn about the latest news.

【小白课程】openKylin便签贴的设计与实现

2024-03-05 09:36:46

openKylin便签贴作为侧边栏的一个小插件,提供便捷的文本记录和灵活的页面展示。openKylin便签贴分为两个部分:便签列表便签页。其中,便签列表以列表形式展示所有内容,可切换图标/列表视图,并提供搜索查找功能;便签页提供编辑内容功能,可对内容进行字体大小/颜色、斜体、下划线、有序/无序、插入图片等操作。

openKylin便签贴及相关软件包安装

$sudo apt install ukui-notebook

注意:openKylin 1.0.1及2.0 Alpha版本均已预装


一.openKylin便签贴功能介绍



1.便签列表

openKylin(开放麒麟)

  • 实时按照修改时间倒序排序

  • 显示每条便签的修改时间和部分文本内容

  • 新建:列表条目增加并打开一个便签页

  • 搜索:匹配列表中所有便签的文本内容进行搜索

  • 删除:删除当前列表选中条目,删除后自动选中列表中上一条便签,若删除时,对应条目的便签为打开状态,则同时关闭此便签页;若无列表中无条目选中,则删除无效

  • 支持双击列表/图标条目,打开或重新激活置顶便签并获取输入焦点


2.便签页

openKylin(开放麒麟)

  • 支持文本修改自动保存

  • 支持用户自定义便签头颜色并保存数据库

  • 文本修改后,此便签页对应便签列表中条目自动置顶排序

  • 便签头颜色修改后,此便签页对应便签列表中条目自动更新同步

  • 删除此便签:删除此便签,并删除此便签页对应便签列表中对应条目

  • 打开便签:任意便签可重新唤起便签列表

  • 新建:在任一便签页新建会创建新便签页,同步到便签

  • 关闭:关闭当前便签页,若当前便签页文本内容为空,则删除此便签,并删除此便签页对应便签列表中条目

  • 支持加粗、斜体、下划线、删除线、无序列表、有序列表

  • 支持修改字体大小,字体颜色


二.openKylin 便签贴实现原理



便签贴基于QT实现,主要涉及便签列表的QListView类和便签编辑页的QTextEdit类。所以在讲便签贴具体实现之前,简单介绍一下这两个类。


1. QListView

QListView可以用来以列表的形式展示数据,在Qt中使用model/View结构来管理数据与视图的关系,model负责数据的存取,数据的交互通过delegate来实现.

(1)数据模型

  • QT提供了一些现成的models用于处理数据项:

  • QStringListModel 用于存储简单的QString列表。

  • QStandardItemModel 管理复杂的树型结构数据项,每项都可以包含任意数据。

  • QDirModel 提供本地文件系统中的文件与目录信息。

  • QSqlQueryModel, QSqlTableModel,QSqlRelationTableModel用来访问数据库。


模型中的每个数据项都有一个与之对应的role来存储某一类数据。需要存取自定义数据可以使用UserRole,UserRole+1...

便签使用QAbstractListModel,自定义了可编辑列表模型noteModel。便签定义NoteRole存储数据对象:

enum NoteRoles{        NoteID = Qt::UserRole + 1,        NoteFullTitle,        NoteCreationDateTime,        NoteLastModificationDateTime,        NoteDeletionDateTime,        NoteContent,        NoteScrollbarPos,        NoteColor,        NoteMdContent,    };

数据的存取:

QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) Q_DECL_OVERRIDE;

(2)自定义delegate

模型的交互和绘制通过自定义delegate来实现。便签定义了两种delegate——list和icon,这两种基本只在item的绘制有区别。

openKylin(开放麒麟)


2.QTextEdit

QTextEdit类是一个多行文本框控件,可以显示多行文本内容,当文本内容超出控件显示范围时,可以显示水平垂直滚动条,用于编辑和显示纯文本和富文本。

操作函数:

  • setPlainText() 设置多行文本框的内容

  • toPlainText() 返回多行文本框的文本内容

  • setHtml() 设置多行文本框的文本内容为HTML文档,HTML文档是描述网页的

  • toHtml() 返回多行文本框的HTML内容

  • clear() 清除多行文本框的内容


信号:

  • textChanged():文本改动信号

  • currentCharFormatChanged(const QTextCharFormat &format):文本风格改动信号

  • void cursorPositionChanged():光标位置变化信号

  • QTextEdit常与QTextCursor一起使用,提供接口进行编辑。


常用函数:

  • beginEditBlock() endEditBlock():分组游标操作

  • insertBlock() 将新文本块(段落)插入光标位于光标位置的文档,并将光标移动到新块的开头。

  • insertFragment() 将现有文本片段插入到光标位置的文档中。

  • insertImage() 将图像插入到光标位置的文档中。

  • insertText() 在光标位置将文本插入到文档中。

  • insertFrame() 在光标的当前块之后将框架插入到文档中,并将光标移动到新框架中空块的开始。

  • insertList() 在光标位置将列表插入到文档中,并将光标移动到列表中第一个项目的开始。

  • insertTable() 在光标的当前块之后将表插入到文档中,并将光标移动到表后块的开始。


三.openKylin 便签贴具体实现



1. 便签列表

首先,介绍一下便签列表涉及的几个类。

  • NoteData 便签数据类,记录便签id,头颜色,标题,最新编辑时间等内容

  • NoteModel 继承QAbstractListModel,便签列表模型抽象类,展示和管理列表数据。

  • QModelIndex  可以用来引用模型中的项,它包含确定这个项在模型中的位置所需的所有信息。索引拥有行信息、列信息,可以使用row()、column()和parent()函数来获取这些信息。为noteModel 模型和noteView 列表提供“桥梁”,供索引。

  • NoteView 继承Qlistview,设置NoteModel 为模型,并显示listview。最终自定义的列表模型中的数据以列表形式显示。

(1) 新建

openKylin(开放麒麟)

首先确定是否有其他正在进行的操作,若无则可以开始新建便签操作,并将列表滚动到最高处。m_noteCounter记录便签数,加一。产生新便签数据类,插入新便签到便签模型中,并将数据保存到数据库中。

void Widget::createNewNote(){  if (!m_isOperationRunning) {        m_isOperationRunning = true;        m_noteView->scrollToTop();        ++m_noteCounter;        NoteData *tmpNote = generateNote(m_noteCounter);        // insert the new note to NoteModel        QModelIndex indexSrc = m_noteModel->insertNote(tmpNote, 0);        // update the editor header date label        QString dateTimeFromDB = tmpNote->lastModificationdateTime().toString(Qt::ISODate);        QString dateTimeForEditor = getNoteDateEditor(dateTimeFromDB);        // 从排序过滤器模型返回与给定 indexSrc 对应的源模型索引。        m_currentSelectedNoteProxy = m_proxyModel->mapFromSource(indexSrc);        saveNoteToDB(m_currentSelectedNoteProxy);        // 设置索引 m_currentSelectedNoteProxy 所在的页面为当前页面        m_noteView->setCurrentIndex(m_currentSelectedNoteProxy);        m_isOperationRunning = false;}......}

(2) 便签列表

双击列表项打开便签,或者右键弹出操作菜单,可打开、删除和清空列表。

openKylin(开放麒麟)

  • 打开:在滚动区域单机便签,为取消突出显示上一个选定的便签。如果在临时便签存在时选择便签,即为删除临时便签,突出显示所选便签,并将所选便签内容加载到textedit。

  • 删除:通过当前列表模型获取noteid,删除对应noteid的便签项,保存到数据库。

  • 清空:清空便签模型、列表等所有信息,并删除数据库内容。

(3)内容搜索

openKylin(开放麒麟)

// 搜索栏文本输入connect(m_searchLine, &QLineEdit::textChanged, this, &Widget::onSearchEditTextChanged);

使用Queue队列获取搜索栏文本内容,将用于过滤模型内容的固定字符串设置为给定模式,根据过滤模型的noteid,显示筛选后的listview.

    void Widget::onSearchEditTextChanged(const QString &keyword){    m_searchQueue.enqueue(keyword);
        if (!m_isOperationRunning) {        m_isOperationRunning = true;        // disable animation while searching        m_noteView->setAnimationEnabled(false);
            while (!m_searchQueue.isEmpty()) {            qApp->processEvents();            QString str = m_searchQueue.dequeue();            if (str.isEmpty()) {                clearSearch();            } else {                m_noteView->setFocusPolicy(Qt::NoFocus);                // 过滤                findNotesContain(str);            }        }
            m_noteView->setAnimationEnabled(true);        m_isOperationRunning = false;    }}void Widget::findNotesContain(const QString &keyword){    // 将用于过滤源模型内容的固定字符串设置为给定模式    m_proxyModel->setFilterFixedString(keyword);    // 如果匹配到不止一行    if (m_proxyModel->rowCount() > 0) {        selectFirstNote();    } else {        m_currentSelectedNoteProxy = QModelIndex();    }}



    (4)视图切换

    openKylin(开放麒麟)

    视图有列表视图和图标视图,自定义两种模型代理iconViewModeDelegate和listViewModeDelegate。

    m_proxyModel->setSourceModel(m_noteModel);          // 代理真正的数据模型,对数据进行排序和过滤m_proxyModel->setFilterKeyColumn(0);                // 此属性保存用于读取源模型内容的键的列,listview只有一列所以是0m_proxyModel->setFilterRole(NoteModel::NoteMdContent);// 此属性保留项目角色,该角色用于在过滤项目时查询源模型的数据m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);//m_noteView->setItemDelegate(new iconViewModeDelegate(m_noteView));    // 安装定制delegate提供编辑功能m_noteView->setModel(m_proxyModel); // 设置view的model是proxyModel,proxyModel作为view和QAbstractListModel的桥


    2.便签页

    便签页操作区:

    openKylin(开放麒麟)

    (1)文本编辑

    文本编辑是基于QTextEdit实现的。

    connect(ui->textEdit, &QTextEdit::textChanged, this, &EditPage::textChangedSlot);void EditPage::textChangedSlot(){    emit texthasChanged(m_noteId, this->m_id);}

    绑定m_noteId为便签id,m_id为当前编辑页面id。两个一起确定编辑的便签页面。通过此信号texthasChanged()传递给便签列表页面,实时更新对应列表项标题等内容。

    connect(m_editors[m_editors.size() - 1], SIGNAL(texthasChanged(int,int)), this,   SLOT(onTextEditTextChanged(int,int)));

    (2)便签头

    自定义便签头noteHead,继承QWidget,显示便签头颜色。便签头菜单noteHeadMenu,也是一个QWidget。包括新建按钮、调色板、选项和关闭按钮。

    点击调色板按钮,可弹出调色板菜单,这个菜单由PaletteWidget.ui实现。

    点击以上调色按钮选择颜色后,以选择红色为例:

    void EditPage::redBtnSlot(){    QColor color((PaletteWidget::KY_RED));    m_editColor = color;    emit colorhasChanged(m_editColor, m_noteId);    m_noteHead->colorWidget = color;    m_noteHeadMenu->colorWidget = color;    update();}

    设置便签头颜色为红色并更新,同时给便签列表窗口发送颜色改变信号colorhasChanged(),便签列表收到信号后更新列表头颜色。

    connect(m_editors[m_editors.size() - 1], SIGNAL(colorhasChanged(QColor,int)), this, SLOT(onColorChanged(QColor,int)));

    (3)字体风格

    字体风格包括字体大小、颜色、加粗、下划线等。首先以字体大小/颜色按钮组为例,介绍一下。

    • 按钮组

      CustomPushButtonGroup继承QFrame,通过加载qss样式文件设置字体大小/颜色按钮组。

    • 按钮下拉选项

      字体大小下拉选项SetFontSize和字体颜色下拉选项SetFontColor,都是基于QListWidget编写的widget窗口。

    // 字体颜色大小connect(m_setSizePage->ui->listWidget, &QListWidget::itemClicked, this,            &EditPage::setFontSizeSlot);connect(m_setColorFontPage->ui->listWidget, &QListWidget::itemClicked, this,            &EditPage::setFontColorSlot);

    以字号修改为例,字体大小和颜色都是采用QTextCharFormat对象实现的。

    // 字号void EditPage::setFontSizeSlot(){    int num = m_setSizePage->ui->listWidget->currentRow();    ui->fontTwinButtonGroup->getFontSizeBtn()->setButtonSize(QString::number(num+10));    m_setSizePage->close();    update();    QTextCharFormat fmt;    InformationCollector::getInstance().addMessage(QString("set font size to %1.").arg(num+10));    fmt.setFontPointSize(num+10);    mergeFormatOnWordOrSelection(fmt);}

    选择字号后,设置QTextCharFormat文本格式,根据格式作以下操作:

    void EditPage::mergeFormatOnWordOrSelection(const QTextCharFormat &format){    QTextCursor cursor = ui->textEdit->textCursor();    if (!cursor.hasSelection()) {        // cursor.select(QTextCursor::WordUnderCursor);    }    cursor.mergeCharFormat(format);    ui->textEdit->mergeCurrentCharFormat(format);    ui->textEdit->setFocus(Qt::TabFocusReason);}

    获取控件的焦点,假设当前控件上的文本并没有被选中,就指定光标区域所在的词为高亮选定词,从而设置字体风格样式。其他的字体风格,如加粗、斜体等:

    • 加粗:

      fmt.setFontWeight(QFont::Bold);

    • 斜体:

      fmt.setFontItalic(QFont::StyleItalic);

    • 下划线:

      fmt.setFontUnderline(ui->fontPropertyWidget->underlineBtn()->isCheckable());

    • 删除线:

      fmt.setFontStrikeOut(ui->fontPropertyWidget->strikeOutBtn()->isCheckable());

    (4)图片插入

    插入图片和字体风格类似,采用对QTextImageFormat 对象进行操作。在光标处进行图片插入。

    connect(ui->fontPropertyWidget->insertBtn(), &QPushButton::clicked, this, &EditPage::insertpicture);void EditPage::insertpicture(){    ......    QTextCursor cursor = ui->textEdit->textCursor();    if(cursor.atStart())    {        m_isInsImg = true;    }    QTextImageFormat imageFormat;    imageFormat.setWidth  ( image.width() );    imageFormat.setHeight ( image.height() );    imageFormat.setName   ( QString("data:image/%1;base64,%2")                                .arg(QString("%1.%2").arg(rand()).arg(format))                                .arg(base64l.data())                                );cursor.insertImage    ( imageFormat );}

    (5)有序/无序列表

    对QTextListFormat对象进行操作,listFmt.setStyle(style)设置列表样式,无序为QTextListFormat::ListDisc,有序为QTextListFormat::ListDecimal。

     connect(ui->fontPropertyWidget->unorderedBtn(), &QPushButton::clicked, this, &EditPage::setUnorderedListSlot);connect(ui->fontPropertyWidget->orderedBtn(), &QPushButton::clicked, this, &EditPage::setOrderedListSlot);
          QTextListFormat listFmt;        if (cursor.currentList()) {            listFmt = cursor.currentList()->format();        }        listFmt.setStyle(style);      cursor.createList(listFmt);

    上述代码首先检查游标是否在现有列表中,如果是,则为新列表的列表格式提供适当的缩进级别。这允许创建嵌套列表,增加缩进级别。更复杂的实现还将对列表每个级别的项目符号使用不同的符号。


    3.pc/平板模式切换

    openKylin平板模式下,便签贴支持全屏化,隐藏最大化按钮,dbus信号监听模式切换。

    QDBusConnection::sessionBus().connect(KYLIN_ROTATION_SERVICE, KYLIN_ROTATION_PATH, KYLIN_ROTATION_INTERFACE,QString("rotations_change_signal"), this, SLOT(rotationChanged(QString)));QDBusConnection::sessionBus().connect(KYLIN_ROTATION_SERVICE, KYLIN_ROTATION_PATH, KYLIN_ROTATION_INTERFACE,QString("mode_change_signal"), this, SLOT(modeChanged(bool)));
    模式切换:

    openKylin(开放麒麟)


    4. 数据同步

    便签使用SQLite数据库对数据增删改查操作,具体表结构如下:


    id

    INTEGER

    便签在数据库中唯一标识,用以区分不同便签

    creation_date

    INTEGER

    便签创建日期

    modification_date

    INTEGER

    便签修改日期

    deletion_date

    INTEGER

    便签删除日期

    content

    TEXT

    便签内容,以html形式存储

    full_title

    TEXT

    便签标题

    note_color

    INTEGER

    便签头颜色

    md_content

    TEXT

    文本化的数据内容,用于实现便签的查找操作


    以上就是openKylin便签贴实现原理,界面的加载逻辑简单,其中由于便签编辑控件选择的是textedit,对于获取图片插入状态,缩进控制等相关功能存在问题,这也是后续openKylin便签需要继续改进的地方。欢迎感兴趣的小伙伴加入我们~