高仿富途牛牛-组件化(六)-炒鸡牛逼的布局记忆功能(序列化和反序列化)
一、布局记忆
一款优秀的软件,不仅仅要求功能强健、稳定性高和可靠的精准率,往往很多时候我们都需要去关注用户界面是否友好,用户操作是否顺畅,软件跨机器使用到底咋样。
说起到怎么让用户交互友好,这就是用户体验和视觉设计师的主场啦。这里我就不多说了,今天主要是想说明一个问题--布局记忆功能
现在客户端软件各式各样,种类多了去了,但是不知道大家有没有注意到有这么一些交互上的细节。
-
使用过QQ的同学应该都比较清楚。我们在QQ使用时,除过第一次登录QQ软件,其余时间段登录QQ时,QQ的初始位置往往会是上一次退出时的位置
-
windows资源管理器我们大家应该都经常在使用,不知道大家有没有仔细观察。我们修改了资源管理器窗口大小后,再次打开资源管理器窗口时,新的窗口大小和我们之前修改后的窗口大小一样。
-
firefox邮件客户端,大家都用过吧,也是会记忆窗口最后
-
还有一些工具软件,比如说PicPick,选择的使用模式会一直记录
-
QQ飞车是一款腾讯出的客户端游戏,他支持多种显示模式,设置一次后,会一直生效,直到我们再次设置为止。
以上是我随便写的几个数据记忆的事例,相信大家都不陌生。除过这些简单的数据持久化以外,其实还有很多其他的事例,这里就不一一例举了。
今天我们主要是想给大家展示下我们负责窗口布局是怎么进行布局记忆的。
二、效果展示
窗口布局记忆如效果图所示。
当我们通过主窗口关闭了软件时,程序会自动把布局信息序列化成字符串,然后进行保存。
再次启动软件时,我们首先会去加载序列化的布局信息,然后进行解析布局信息,并构造我们的窗口,这个工程称之为反序列化。
三、重点回顾
1、窗口管理
之前我已经写了好几天文章都是讲组件化相关的东西,其中有一篇文章高仿富途牛牛-组件化(五)-如何去管理炒鸡多的小窗口主要是讲解怎么去管理过多的小窗口,主要是把创建的过程进行了封装,让外界使用起来更加的接口化。
本篇文章主要是讲述布局怎么去记忆?记忆后又是怎么去恢复?关于窗口创建和消息通信这里我就不在去讲解了。感兴趣的同学可以翻看之前的文章,因为我的这个demo是在一直的维护,更新过程中,因此讲到这篇文章的时候,之前的一些主题中的方式、方法可能已经发现了变化,如果有问题的欢迎留言。
2、页签TabButton
一个组件窗口中同时只允许一个页签被选中,选中另一个页签时,其他的页签都会被重置为非选中状态。
TabButton是一个复杂的小窗口,支持同一个工具栏内拖拽,也支持多个工具栏之间拖拽。
3、子面板SubPanel
每一个组件窗口都包含有多个页签和多个SubPanel,其中SubPanel和页签时一对一的关系。
我们切换页签的时候,SubPanel也会跟随者切换,而每一个SubPanel上都包含有不同的小窗口,这些小窗口都是由工具箱进行创建的。
工具箱这里就不在多说了,看展示的效果图,上边就有一个工具箱窗口,当我们点击其上的工具按钮时,就会在当前的SubPanel上创建一个对应的小窗口。
四、布局记忆内容
首先我一直强调的是高仿富途牛牛-组件化,因此这里记忆的内容我也是根据福牛的交互行为来记忆的,可能记忆的内容有下面这些,但也可能更多。
- 组件窗口个数
- 组件窗口位置和大小,包括层次关系
- 组件窗口关联的工具箱是否显示和其位置
- 工具栏的状态,包括工具按钮状态,页签个数、顺序、名称和当前选中项
- 子面板上的小窗口
- 小窗口的层次关系、位置和大小
以上内容就是我们序列化时会存储的信息,但又不仅限于这些。
五、布局信息序列化
要让布局信息持久化,那么布局信息必然要被我们存储到硬盘上,因此电脑上的内存信息系统重启后就会消息。
好,那么接下来就是考虑把布局信息写入硬盘,这个时候我们就得找个合适的实际写入时机,目前我写入的时机是在关闭软件的时候,但是这里不建议大家也这么搞,因此这回导致关节关闭有延迟,当我们有大量的数据需要写入的时,可能会影响用户体验。
关于写入时机选择,不是本篇文章讨论的主要内容,感兴趣的可以自己去研究。
数据写入时需要注意,给读取数据时写入一些标志,否则读取数据时如果包含一些循环,则不知道循环应该什么时候结束。
1、流程
- 主组件窗口关闭时,开始序列化布局信息
- 首先写入组件窗口个数,方便后期读数据
- 工具栏按钮状态写入
- 工具栏页签个数写入
- 工具栏页签循环写入
- 工具栏页签选中项index写入
- 工具箱大小和位置写入
- 循环子面板SubPanel
- 写入SubPanel中所有小窗体信息
- 小窗体信息吸入:标题栏名称、所属组、窗口大小、位置等
2、主流程写入
窗口信息使用二进制的方式写入文件,由于现在是demo阶段,因此这里为了方便测试,随手写了一个文件路径。
void TemplateLayout::SaveMainLayout()
{
Q_ASSERT(m_pToolBar);
QString path = "d:\\main.ttlayout";
QFile file(path);
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
QDataStream in(&file);
int count = templates.size();
in << count;//存储组件窗口个数
//从最下面一级的窗体开始序列化
for (int i = templates.size() - 1; i >= 0; --i)
{
TemplateLayout * widget = templates.at(i);
widget->m_pToolBar->SaveLayout(in);
in << QString("toolBar");//toolBar结束标志
widget->SaveToolBox(in);
widget->m_pPanel->SaveLayout(in);
in << QString("panels");//panel结束标志
}
}
}
从最下面一级的组件窗体开始序列化,主要创建的时候,就是自下而上创建,窗口的z值就不存在问题。
序列化代码主体流程看起来就像上边这样,我们使用QDataStream来进行二进制信息的写入。
在整个写入的过程中,我们使用了一个QDataStream对象,并把文件作为他的输入设备。
这里需要注意一点,我们不能在函数调用过程中使用多个QDataStream,把每个窗口的布局信息都存储到一个QByteArray中去。因为QDataStream内部在存储数据时,会在末尾加上4个字节的结束符,这样我们在多层嵌套写数据时,虽然没有问题,但是读数据时就会出现问题,这个问题我也是查了好久就通过调试代码发现的
3、标签页写入
前边我们也说了,我们整个的写入过程都使用了一个QDataStream,内部窗口的写入都是使用了最外层的QDataStream,这里从参数我们也可以看得出来。
标签页写入方式和之前的模式差不多,主要是存储的数据不同,这里主要存放了3种信息:标签页数量、标签页名称和选中项下标
void DragTabWidget::SaveLayout(QDataStream & in) const
{
Q_ASSERT(m_pTabLayout);
in << m_buttonMaps.size();//记录button个数
int selectedIndex = 0;
int buttonIndex = 0;
for (int index = 0; index < m_pTabLayout->count(); ++index)
{
if (TabButton * desButton = dynamic_cast<TabButton *>(m_pTabLayout->itemAt(index)->widget()))
{
in << desButton->Text();
if (desButton->IsSelected())
{
selectedIndex = buttonIndex;
}
++buttonIndex;
}
}
in << selectedIndex;//记录选中按钮
}
4、小窗口写入
小窗口写入时,首先写入了的是标题栏的信息,然后在写入窗口自身的位置、大小和窗口类型
这里需要重点提下窗口类型,这个信息很重要。当我们反序列化的时候,需要根据这个类型来进行创建窗口
void SmallWidget::SaveLayout(QDataStream & in) const
{
QPoint pos = this->pos();//保存位置
QSize size = this->size();//保存大小
SubWindowNormalType type = GetSmallType();//保存窗口类型
m_pTitle->SaveLayout(in);
in << pos;
in << size;
in << (int)type;
}
5、其他
序列化的整个过程基本都是一样的套路,主要就是使用QDataStream对象把布局信息以二级制的形式写入到硬盘文件中。
其他的布局信息写入方式大豆差不多,这里就不一一列出。
六、布局信息反序列化
说完序列化后,接下来就是我们的反序列化的过程了。
反序列化就是序列化的相反过程,主要是我们需要写入正确的信息,然后按写入时的顺序进行读取布局信息即可
1、流程
- 启动程序时,打开布局文件
- 读出组件窗口个数
- 读取工具栏按钮状态
- 初始化页签,这个时候SubPanel也会被初始化
- 初始化页签选中项
- 读取工具箱大小和位置
- 初始化各子面板上的小窗口
- 循环第三步
2、反序列化主流程
反序列化就是序列化的逆序,不过这里需要注意的一个地方就是,我们序列化的时候,主窗口时最后保存的,因此反序列化的时候,主窗口也是最后才进行初始化的。
注意代码中的if (i == count - 1)这个if判断,就是处理主窗口初始化。
void TemplateLayout::RestoreLayout()
{
QString path = "d:\\main.ttlayout";
QFile file(path);
if (file.open(QIODevice::ReadOnly))
{
QDataStream out(&file);
int count;
out >> count;//存储组件窗口个数
for (int i = 0; i < count; ++i)
{
TemplateLayout * widget = nullptr;
if (i == count - 1)//最后一个是主窗口
{
widget = this;
}
else
{
widget = new TemplateLayout;
widget->setWindowFlags(Qt::FramelessWindowHint);
widget->m_pToolBar->SetMoveable(true);
widget->SetIsMajor(false);
widget->show();
}
widget->m_pToolBar->LoadLayout(out);
QString toolSign;
out >> toolSign;//toolBar结束标志
Q_ASSERT(toolSign == "toolBar");
widget->LoadToolBox(out);
widget->m_pPanel->LoadLayout(out);
QString panelSign;
out >> panelSign;//panel结束标志
Q_ASSERT(panelSign == "panels");
}
}
}
3、工具栏按钮
读取工具栏按钮的信息,并进行初始化。
工具栏按钮主要是有两个
- 小工具窗口是否打开
- 磁力吸附特性是否启用。
代码中toolBoxChecked就是表示工具箱按钮是否被选中,magneticChecked表示吸力吸附按钮是否被选中
void DragToolBar::LoadLayout(QDataStream & out)
{
bool toolBoxChecked, magneticChecked;
out >> toolBoxChecked;
out >> magneticChecked;
Q_ASSERT(m_pToolBoxAct);
m_pToolBoxAct->setChecked(toolBoxChecked);
m_pToolBoxAct->triggered(toolBoxChecked);
Q_ASSERT(m_pMagneticAct);
m_pMagneticAct->setChecked(magneticChecked);
m_pMagneticAct->triggered(magneticChecked);
Q_ASSERT(m_pDragTab);
m_pDragTab->LoadLayout(out);
}
4、初始化标签页
加载工具栏上标签页,分3个步骤
- 读取标签页个数
- 循环读取所有标签页
- 读取选中的标签页下标
根据读取到的信息初始化工具栏。
void DragTabWidget::LoadLayout(QDataStream & out)
{
int count;
out >> count;
QStringList titles;
while (count-- > 0)
{
QString title;
out >> title;
titles.append(title);
}
int selectedIndex = 0;
out >> selectedIndex;
TabButton * selected = nullptr;
for (int i = 0; i < titles.size(); ++i)
{
QString title = titles.at(i);
UpdateMaxOrder(title);
TabButton * button = AddNewButton(title);
if (i == selectedIndex)
{
selected = button;
}
}
if (selected)
{
ButtonClicked(selected->GetID());
}
}
5、子面板初始化
在布局信息序列化小结中,我们讲述了子面板中的小窗口在写入信息时,写入了窗口的类型type,这个时候我们就会发现这个type真的太重要了
看如下代码,我们读出了小窗口的type值,然后使用SmallFactory工厂的CreateWidget方法创建了小窗口,代码看起来是不是还是比较流畅的。
除过窗口类型外,还包括了窗口标题栏名称、所属组、位置、大小等信息
void SubContentWidget::LoadeLayout(QDataStream & out)
{
QString titleName, groupName;
QPoint pos;
QSize size;
int type;
int count;
out >> count;
while (count-- > 0)
{
out >> titleName;
out >> groupName;
out >> pos;//保存位置
out >> size;//保存大小
out >> (int)type;//保存窗口类型
SmallWidget * smallWidget = SmallFactory::GetInstance()->CreateWidget(SubWindowNormalType(type), this);
AddSmallWidget(smallWidget);
smallWidget->SetWindowTitle(titleName);
if (groupName.isEmpty() == false)
{
smallWidget->SetToolText(STT_GROUP, groupName);
}
smallWidget->move(pos);
smallWidget->resize(size);
smallWidget->show();
}
}
6、其他
反序列化的整个过程基本都是一样的套路,主要就是使用QDataStream对象把布局信息以二级制的形式读入到内存中。
其他窗口的反序列化操作基本类似,这里就不一一列出。
七、相关文章
以上的内容,基本上就是本篇文章的内容所有内容啦!序列化和反序列化功能基本完成,希望可以帮到大家。
很重要--转载声明
-
本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
-
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。