【小白课程】openKylin便签贴的设计与实现
openKylin便签贴作为侧边栏的一个小插件,提供便捷的文本记录和灵活的页面展示。openKylin便签贴分为两个部分:便签列表和便签页。其中,便签列表以列表形式展示所有内容,可切换图标/列表视图,并提供搜索查找功能;便签页提供编辑内容功能,可对内容进行字体大小/颜色、斜体、下划线、有序/无序、插入图片等操作。
openKylin便签贴及相关软件包安装:
$sudo apt install ukui-notebook
注意:openKylin 1.0.1及2.0 Alpha版本均已预装
1.便签列表
实时按照修改时间倒序排序
显示每条便签的修改时间和部分文本内容
新建:列表条目增加并打开一个便签页
搜索:匹配列表中所有便签的文本内容进行搜索
删除:删除当前列表选中条目,删除后自动选中列表中上一条便签,若删除时,对应条目的便签为打开状态,则同时关闭此便签页;若无列表中无条目选中,则删除无效
支持双击列表/图标条目,打开或重新激活置顶便签并获取输入焦点
2.便签页
支持文本修改自动保存
支持用户自定义便签头颜色并保存数据库
文本修改后,此便签页对应便签列表中条目自动置顶排序
便签头颜色修改后,此便签页对应便签列表中条目自动更新同步
删除此便签:删除此便签,并删除此便签页对应便签列表中对应条目
打开便签:任意便签可重新唤起便签列表
新建:在任一便签页新建会创建新便签页,同步到便签
关闭:关闭当前便签页,若当前便签页文本内容为空,则删除此便签,并删除此便签页对应便签列表中条目
支持加粗、斜体、下划线、删除线、无序列表、有序列表
支持修改字体大小,字体颜色
便签贴基于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的绘制有区别。
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() 在光标的当前块之后将表插入到文档中,并将光标移动到表后块的开始。
1. 便签列表
首先,介绍一下便签列表涉及的几个类。
NoteData 便签数据类,记录便签id,头颜色,标题,最新编辑时间等内容
NoteModel 继承QAbstractListModel,便签列表模型抽象类,展示和管理列表数据。
QModelIndex 可以用来引用模型中的项,它包含确定这个项在模型中的位置所需的所有信息。索引拥有行信息、列信息,可以使用row()、column()和parent()函数来获取这些信息。为noteModel 模型和noteView 列表提供“桥梁”,供索引。
NoteView 继承Qlistview,设置NoteModel 为模型,并显示listview。最终自定义的列表模型中的数据以列表形式显示。
(1) 新建
首先确定是否有其他正在进行的操作,若无则可以开始新建便签操作,并将列表滚动到最高处。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) 便签列表
双击列表项打开便签,或者右键弹出操作菜单,可打开、删除和清空列表。
打开:在滚动区域单机便签,为取消突出显示上一个选定的便签。如果在临时便签存在时选择便签,即为删除临时便签,突出显示所选便签,并将所选便签内容加载到textedit。
删除:通过当前列表模型获取noteid,删除对应noteid的便签项,保存到数据库。
清空:清空便签模型、列表等所有信息,并删除数据库内容。
(3)内容搜索
// 搜索栏文本输入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)视图切换
视图有列表视图和图标视图,自定义两种模型代理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.便签页
便签页操作区:
(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)));
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便签需要继续改进的地方。欢迎感兴趣的小伙伴加入我们~