一、布局記憶
一款優秀的軟件,不僅僅要求功能強健、穩定性高和可靠的精准率,往往很多時候我們都需要去關注用戶界面是否友好,用戶操作是否順暢,軟件跨機器使用到底咋樣。
說起到怎么讓用戶交互友好,這就是用戶體驗和視覺設計師的主場啦。這里我就不多說了,今天主要是想說明一個問題--布局記憶功能
現在客戶端軟件各式各樣,種類多了去了,但是不知道大家有沒有注意到有這么一些交互上的細節。
-
使用過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
-
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。