Qt之高DPI顯示器(二) - 自適配解決方案分析
目錄
- 一、回顧
- 二、框架說明
- 1、ICallDPIChanged
- 2、IDPIHelper
- 3、懸浮窗體管理器
- 三、方案分析
- 1、視窗大小
- 2、字型大小
- 3、間距
- 4、圖示
- 四、相關文章
原文連結:Qt之高DPI顯示器(二) - 自適配解決方案分析
最近一直在處理高DPI問題,也花費了不少功夫,前前後後使用了多種解決方案,各種方案也都有利弊,筆者最終採用了自適配方案,雖然複雜一些,但是結果可控。這裡把處理的過程記錄下來,留給有同樣需求的同學
一、回顧
上一篇文章Qt之高DPI顯示器(一) - 解決方案整理講述了筆者處理高DPI顯示的一系列分析過程,為了更好的閱讀和排版,其中有一些實驗方案沒有具體寫出,即使寫出來也沒有多大用處,而且會影響大家閱讀。
本篇文章將會接著上一篇文章的最後一小節-自適配高DPI
進行講解,由於內容比較多,而且整個解決方案程式碼量也會相當大,因此文章中也只會涉及到整個DPI適配架構的核心和一些關鍵程式碼,有疑問歡迎提問
上一篇文章提到了T視窗,那麼什麼是T視窗呢!下面我們來具體分析。
這裡筆者貼一個適配完成以後的TWidget類,大家可以先分析分析,也可以猜猜看,每一處程式碼的具體含義。所有程式碼細節筆者後邊會具體分析每一處細節
//函式宣告 //xxx.h #define CreateTWidget() CreateObject(Widget) class TWidget : public QWidget, public ICallDPIChanged { Q_OBJECT public: TWidget(float scale, QWidget * parent = nullptr); TWidget(QWidget * parent = nullptr);//不建議使用 TWidget(QWidget * parent, Qt::WindowFlags f);//不建議使用 ~TWidget(); public: //重寫大小變化相關函式 DECLARE_RESIZE(); void setLayout(QLayout * layout); public: //QWidget virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result)override; //ICallDPIChanged DECLARE_DPI(); //TWidget virtual void AdjustReiszeHandle(); DECLARE_DPI_SYMBOL; protected: WidgetResizeHandler resize_handler;// 用於支援放大縮小 拖拽 等功能 private: TigerUILib::ReiszeActions m_sizeActions; QSize m_size; QSize m_minimumSize; QSize m_maximumSize; ICallDPIChanged * m_pLayout = nullptr;//DPI發生變化時 通知佈局 }; //函式實現 //xxx.cpp TWidget::TWidget(float scale, QWidget * parent) : QWidget(parent) , dpi_scale(scale) { } TWidget::TWidget(QWidget * parent /*= nullptr*/) : QWidget(parent) { dpi_scale = WINDOW_SCALE; } TWidget::TWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) { dpi_scale = WINDOW_SCALE; } TWidget::~TWidget() { DPIHelper()->RemoveDPIRecord(WINDOW_WINID); } void TWidget::setLayout(QLayout * layout) { WIDGET_RELEATE_LAYOUTS(layout); __super::setLayout(layout); } DEFINE_RESIZE(Widget); DEFINE_DPI(Widget); bool TWidget::nativeEvent(const QByteArray &eventType, void *message, long *result) { MSG* pMsg = reinterpret_cast<MSG*>(message); switch (pMsg->message) { case WM_DPICHANGED: { DWORD dpi = LOWORD(pMsg->wParam); WId id = WINDOW_WINID; if (DPIHelper()->DPIChanged(dpi, id)) { ScaleChanged(DPIHelper()->GetDPIScale(id)); RefrushSheet(this, id); } } } return __super::nativeEvent(eventType, message, result); } void TWidget::ScaleChanged(float scale) { DEFINTE_SCALE_RESIZE(Widget); if (m_pLayout) { m_pLayout->ScaleChanged(scale); } AdjustReiszeHandle(); } void TWidget::AdjustReiszeHandle() { if (resize_handler.isWidgetMoving()) { resize_handler.dpiChanged(WINDOW_SCALE); } }
二、框架說明
用一段話描述一下DPI適配方案?
答:筆者提到的DPI適配方案其實原理很簡單,沒有想象中那麼複雜,方案也是中規中矩,其中遵守以下這麼幾條大的原則
- 首先就是window窗體自己去監測自身所在螢幕DPI發生變化,發生變化時通知佈局進行縮放
- 區域性縮放後,然後對自身所包含的widget窗體和佈局進行縮放
- 不在佈局中的窗體需要單獨去控制縮放
是不是說起來很簡單,但是要實現這麼一個流程還是有一些難度的,首先考慮的就是效率,如果做完效率跟不上那麼一切都是瞎扯。
為了更好的效率,筆者也是做了不需要的優化,優化的內容不在本篇文章中進行討論,後續會單獨分出一篇文章說明
下面是兩個DPI適配框架的核心介面類,分別是DPI發生變化時的回撥介面類和DPi管理介面類
struct ICallDPIChanged
{
virtual void ScaleChanged(float scale) = 0;
virtual WId GetWID() const = 0;
virtual void SetScale(float scale) = 0;
};
#define STANDARD_DPI 96.0
struct IDPIHelper
{
virtual bool DPIChanged(unsigned short, WId) = 0;
virtual void RemoveDPIRecord(WId) = 0;//移除指定native窗體的DPI記錄 一般用於native窗體析構時
virtual float GetDPIScale(WId) const = 0;
virtual float GetOldDPIScale(WId) const = 0;
virtual QString GetStyleSheet(WId) const = 0;//獲取指定DPI下的樣式表
virtual float GetScaleNumber(float, WId) const = 0;//獲取指定DPI下的數值 縮放後數值
virtual QList<WId> GetAllWindowID() const = 0;//獲取所有自己載入過面板的視窗ID
//優化介面 主要是為了適配使用者主機只有一種DPI時使用
virtual bool IsOnlyOneDPI() const = 0;//獲取使用者主機是否只有一種DPI
virtual void RefrushDPIRecords() = 0;//顯示器數量發生了變化 刷新歷史顯示器DPI記錄
virtual void SetDefaultScale(float scale) = 0;//設定預設DPI值 當顯示器dpi只有一種時重新整理
virtual float GetDefaultScale() const = 0;//獲取預設DPI縮放值 只有當機器上所有的顯示器為統一dpi時起作用
};
IDPIHelper * GetDPIHelper();
#define DPIHelper() GetDPIHelper()
1、ICallDPIChanged
dpi變化時回撥類,當dpi發生變化時,通過該介面類中的ScaleChanged方法進行處理變動,比如說第一小節中的TWidget類,我們也重寫了這個介面,在該介面中我們對窗體進行了大小適配和佈局適配
物件宣告中的函式宣告使用了巨集進行包裝因此沒有直接顯示出來
void TWidget::ScaleChanged(float scale)
{
DEFINTE_SCALE_RESIZE(Widget);
if (m_pLayout)
{
m_pLayout->ScaleChanged(scale);
}
AdjustReiszeHandle();//如果窗體正在被拖拽需要適配拖拽的位置
}
2、IDPIHelper
IDPIHelper是整個DPi適配的核心模組,他負責整個DPI排程的核心功能,包括:DPI改變檢測、獲取指定window窗體縮放比、獲取指定window窗體的qss內容和獲取指定數值在不同DPI下的實際數值等。除過以上核心介面以外,筆者為了優化DPI適配效果,還增加了一系列優化介面,主要是針對使用者主機只有一種DPI時所作的效能提升。
由於篇幅原因,這裡把一些關鍵實現節點列出來
1、dpi變化入口
如下是dpi發生變化實現介面,函式中幹了三件事
- 首先監測dpi是否正在發生了變化,如果發生了變化則更新快取中的window窗體的dpi縮放比
- 接著讀取window窗體中的qss標識生成新的qss樣式字串
- 通知所有懸浮窗體管理器,適配所有懸浮窗體
懸浮窗體指沒有佈局的窗體,當懸浮窗體的父窗體dpi發生變化時,相應的懸浮窗體也需要進行適配
bool CDPIHelper::DPIChanged(unsigned short dpi, WId id)
{
#ifndef HIGHDPI_ENABLE
return false;
#endif
float scale = dpi / STANDARD_DPI;
RefrushDPIRecords();
if (m_pWindowScale.contains(id))
{
if (m_pWindowScale[id] == scale)
{
return false;
}
m_pWindowOldScale[id] = m_pWindowScale[id];
}
m_pWindowScale[id] = scale;
QWidget * window = QWidget::find(id);
m_strQssFile = window->property(QSS_FIlE).toString();
if (m_strQssFile.isEmpty())
{
m_strQssFile = DEFAULT_QSS_FILE;
}
else
{
if (m_strQssFile.endsWith(DEFAULT_QSS_SUFFIX) == false)
{
m_strQssFile.append(DEFAULT_QSS_SUFFIX);
}
}
RefrushTimesSheet(Skin::TypeDefault, id);
RefrushTimesSheet(Skin::TypeLight, id);
CFloatingWidgetMgr::getInstance()->dpiChanged(id, scale);
return true;
}
2、獲取指定DPI下的qss內容
void CDPIHelper::RefrushTimesSheet(Skin::SKIN_TYPE skin, WId id)
{
float scale = GetDPIScale(id);
int times = (int)(scale + 0.5001);//幾倍圖
//如果基礎qss不存在 則需要從硬碟中讀取
//讀取時按照向上取整進行讀取qss檔案
//如果高分屏qss不存在 則讀取一倍qss檔案
if (m_StyleSheets[skin].size() < times)
{
m_StyleSheets[skin].resize(times);
}
std::wstring filePath = ImagePath::GetSkinFilePath(skin, m_strQssFile.toStdWString(), times);
if (QFile::exists(QString::fromStdWString(filePath)) == false)
{
filePath = ImagePath::GetSkinFilePath(skin, m_strQssFile.toStdWString());
}
QFile qss(QString::fromStdWString(filePath));
qss.open(QFile::ReadOnly);
if (qss.isOpen())
{
QString btnstylesheet = QObject::tr(qss.readAll());
m_StyleSheets[skin][times - 1][SCALE_ENLARGE(m_strQssFile, scale)] = btnstylesheet;
qss.close();
}
Q_ASSERT(m_StyleSheets[skin].size() > times - 1);
//更新快取中的換膚檔案
m_StyleSheetMap[skin][SCALE_ENLARGE(m_strQssFile, scale)] =
QtTigerHelper::ScaleSheet(m_StyleSheets[skin][times - 1][SCALE_ENLARGE(m_strQssFile, scale)], scale);
}
3、懸浮窗體管理器
大多數的窗體都是在佈局中完成的,但是也有一小部分的視窗不在佈局中,需要單獨去適配,這個時候就需要使用CFloatingWidgetMgr佈局管理器。
/**
* 簡介:懸浮視窗管理器 負責在DPI發生變化時通知懸浮視窗
支援如下型別的懸浮視窗:
TFrame TPushButton TLabel TTableView TWidget TDialog TMainWindow
*/
class CFloatingWidgetMgr : public QObject
{
Q_OBJECT
public:
static CFloatingWidgetMgr * getInstance();
public:
void addWidget(QWidget * widget);
//dpi helper call
void dpiChanged(WId id, float scale);
private:
QSet<ICallDPIChanged *> m_pWidgets;
};
懸浮窗體適配高DPI也很簡單,只需要把自己加入到懸浮窗體管理器中即可,是不是也很簡單。
CFloatingWidgetMgr::getInstance()->addWidget(xxx);
三、方案分析
既然我們要重寫Qt控制元件的非virtual介面,那麼這個行為在C++語法上應該叫覆蓋,要想呼叫我們覆蓋的函式,使用多型肯定是不行的,聰明的你肯定也想到了,我們在使用介面類時,只能使用T打頭的控制元件類宣告物件,這樣就會呼叫我們覆蓋後的介面
上一篇文章大致說過,要自適配高DPI我們需要適配四個專案,分別是視窗大小、字型大小、間距和圖示,那麼接下來就開始我們的分析過程
1、視窗大小
要適配軟體視窗大小,我們總共需要重寫如下14個和大小相關函式,而且這只是大小相關的函式,也就是QWidget的介面,其他更復雜的介面需要針對具體的類去重寫
void resize(int w, int h);void resize(const QSize &); void setFixedHeight(int w);
void setFixedWidth(int w);void setFixedSize(int w, int h);void setFixedSize(const QSize &s);
void setMinimumSize(const QSize &);void setMinimumSize(int minw, int minh);
void setMinimumHeight(int minh);void setMinimumWidth(int minw);
void setMaximumSize(const QSize &);void setMaximumSize(int maxw, int maxh);
void setMaximumHeight(int minh);void setMaximumWidth(int minw);
Qt的介面類我粗略估計了下,至少有幾十個,如果每一個類都需要去適配,那麼工作量可想而知,因此筆者想了一個辦法,做了一系列巨集,像下面程式碼這樣,只需要在我們想要適配的類中新增巨集即可
//函式宣告
#define DECLARE_RESIZE()\
void resize(int w, int h);void resize(const QSize &); void setFixedHeight(int w); \
void setFixedWidth(int w);void setFixedSize(int w, int h);void setFixedSize(const QSize &s);\
void setMinimumSize(const QSize &);void setMinimumSize(int minw, int minh);\
void setMinimumHeight(int minh);void setMinimumWidth(int minw);\
void setMaximumSize(const QSize &);void setMaximumSize(int maxw, int maxh);\
void setMaximumHeight(int minh);void setMaximumWidth(int minw);\
實際使用過程類似第一小節那樣,非常簡單。
函式宣告有了,接下來就是函式實現,方法類似,筆者還是寫了一個巨集來適配相關放大函式,程式碼下下面這樣
//函式實現
#define DEFINE_RESIZE(name)\
void T##name::resize(int w, int h){ m_sizeActions |= TigerUILib::RA_Resize; float scale = dpi_scale; m_size = QSize(w, h);;__super::resize(m_size.width() * scale, m_size.height() * scale);}\
void T##name::resize(const QSize & size){ m_sizeActions |= TigerUILib::RA_Resize; float scale = dpi_scale;m_size = size;__super::resize(m_size * scale);}\
void T##name::setFixedHeight(int h){m_sizeActions |= TigerUILib::RA_FixedHeight;float scale = dpi_scale;m_size.setHeight(h);__super::setFixedHeight(m_size.height() * scale);}\
void T##name::setFixedWidth(int w){m_sizeActions |= TigerUILib::RA_FixedWidth;float scale = dpi_scale;m_size.setWidth(w);__super::setFixedWidth(m_size.width() * scale);}\
void T##name::setFixedSize(int w, int h){m_sizeActions |= TigerUILib::RA_FixedSize;float scale = dpi_scale; m_size = QSize(w, h); __super::setFixedSize(m_size.width() * scale, m_size.height() * scale);}\
void T##name::setFixedSize(const QSize & size){m_sizeActions |= TigerUILib::RA_FixedSize;float scale = dpi_scale; m_size = size; __super::setFixedSize(m_size * scale);}\
void T##name::setMinimumSize(const QSize & size){m_sizeActions |= TigerUILib::RA_MinimumSize;float scale = dpi_scale;m_minimumSize = size; __super::setMinimumSize(m_minimumSize * scale);}\
void T##name::setMinimumSize(int w, int h){m_sizeActions |= TigerUILib::RA_MinimumSize;float scale = dpi_scale; m_minimumSize = QSize(w, h); __super::setMinimumSize(m_minimumSize.width() * scale, m_minimumSize.height() * scale);}\
void T##name::setMinimumHeight(int h){m_sizeActions |= TigerUILib::RA_MinimumHeight;float scale = dpi_scale;m_minimumSize.setHeight(h); __super::setMinimumHeight(m_minimumSize.height() * scale);}\
void T##name::setMinimumWidth(int w){m_sizeActions |= TigerUILib::RA_MinimumWidth;float scale = dpi_scale; m_minimumSize.setWidth(w); __super::setMinimumWidth(m_minimumSize.width() * scale);}\
void T##name::setMaximumSize(const QSize & size){m_sizeActions |= TigerUILib::RA_MaximumSize;float scale = dpi_scale; m_maximumSize = size; __super::setMaximumSize(m_maximumSize * scale);}\
void T##name::setMaximumSize(int w, int h){m_sizeActions |= TigerUILib::RA_MaximumSize;float scale = dpi_scale; m_maximumSize = QSize(w, h); __super::setMaximumSize(m_maximumSize.width() * scale, m_maximumSize.height() * scale);}\
void T##name::setMaximumHeight(int h){m_sizeActions |= TigerUILib::RA_MaximumHeight;float scale = dpi_scale; m_maximumSize.setHeight(h); __super::setMaximumHeight(m_maximumSize.height() * scale);}\
void T##name::setMaximumWidth(int w){m_sizeActions |= TigerUILib::RA_MaximumWidth;float scale = dpi_scale; m_maximumSize.setWidth(w); __super::setMaximumWidth(m_maximumSize.width() * scale);}
動態調整
仔細閱讀DEFINE_RESIZE巨集中的任意一個函式,就能發現每一個函式中都有一個TigerUILib::WidgetAction標記,表示該物件的此函式是否被呼叫過,標記之後有一個好處,那就是當我們軟體所在螢幕的DPI發生變化時可以有針對性的去呼叫相關函式,下面是一個簡單的測試程式碼。
if (testflag("setfixedWidth"))
{
setFixedWidth(width * scale);
}
說到這裡有必要介紹下DEFINTE_SCALE_RESIZE巨集,如下程式碼,就不解釋了一看應該都會明白
#define DEFINTE_SCALE_RESIZE(name)\
if (m_sizeActions.testFlag(TigerUILib::RA_FixedWidth)){Q##name::setFixedWidth(m_size.width() * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_FixedHeight)){Q##name::setFixedHeight(m_size.height() * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_FixedSize)){Q##name::setFixedSize(m_size * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_Resize)){ QSize newSize = m_size * scale;if(minimumSize().width() > newSize.width()){Q##name::setMinimumSize(newSize);}Q##name::resize(newSize);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MinimumSize)){Q##name::setMinimumSize(m_minimumSize * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MinimumHeight)){Q##name::setMinimumHeight(m_minimumSize.height() * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MinimumWidth)){Q##name::setMinimumWidth(m_minimumSize.width() * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MaximumSize)){Q##name::setMaximumSize(m_maximumSize * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MaximumHeight)){Q##name::setMaximumHeight(m_maximumSize.height() * scale);}\
if (m_sizeActions.testFlag(TigerUILib::RA_MaximumWidth)){ Q##name::setMaximumWidth(m_maximumSize.width() * scale); }\
dpi_scale = scale;
2、字型大小
Qt程式我們的字型大小都是在qss檔案中進行標記,那麼適配高DPI也就很簡單了,只需要把96dpi下的數字大小按比例進行放大即可。
知道方法後,做起來就很簡單了,只需要寫一個字串替換函式,把qss中的數值按比例放大即可,方法如下。
數值放大時有一個小技巧,那就是要做一個平滑處理,1.49px當做1px處理 1.5px當做2px,意思就是說在做數字當大的過程中,可能會出現小數,我們的原則是數值放大後加上0.50001然後取整數部分。
QString QtTigerHelper::ScaleSheet(const QString & sheet, float scale)
{
if (sheet.isEmpty())
{
return sheet;
}
//1倍圖時不需要做任何處理
if (scale == 1.0)
{
return sheet;
}
//放大字型
QString tempStyle = sheet;
QRegExp rx("\\d+px", Qt::CaseInsensitive);
rx.setMinimal(true);
int index = -1;
while ((index = rx.indexIn(tempStyle, index + 1)) >= 0)
{
int capLen = rx.cap(0).length() - 2;
QString snum = tempStyle.mid(index, capLen);
snum = QString::number(qRound(snum.toInt() * scale));
tempStyle.replace(index, capLen, snum);
index += snum.length();
if (index > tempStyle.size() - 2)
{
break;
}
}
return tempStyle;
}
3、間距
Qt中的佈局有2中方式可以設定,可以在程式碼中通過介面設定,也可以通過qss進行設定,當然了這兩種情況都需要適配。
佈局的margin
記錄呼叫了哪些設定大小的函式,在dpi發生變化時重新設定一遍,類似於視窗大小變化時所作調整
if (testflag("margin"))
{
setContextMargin(...);
}
padding和margin
方式和放大字型一樣,可以通過統一的時機去處理
讀取原有qss檔案,使用正則表示式生成scale版本的新qss檔案。
4、圖示
圖示替換是一個相對來說比較複雜的事情,這裡有必要細說一下。
首先是工程中需要額外新增2x和3x解析度的圖示,1x圖示為正常情況下使用的圖示,2x和3x圖示分別是高解析度下的圖示
替換圖示有兩種情況,一種是使用qss方式貼的圖,另一種是自繪貼的圖
qss方式
預先生成高解析度下的整數倍xxx_2x.qss和xxx_3x.qss檔案,需要強調一下,2x和3xqss檔案中的字號還是一倍程式中的字號,實際使用的時候在動態放大,如果想要程式的效率高一些可能還需要做一些快取
自繪
如果是自繪文字和圖片,那就需要自己控制縮放比,和圖片壓縮係數
縮放比: 繪製文字時需要放大的比例,計算方式為當前dpi值除以96.0,結果是一個浮點數,比如說1.5
壓縮係數: 繪製圖片的時候這裡有一個小竅門,當我們繪製縮放比為小數情況時,需要使用距離較近的整數圖片進行壓縮繪製,這樣的情況我們就需要使用壓縮係數進行動態調整繪製圖片的大小
float ImagePath::GetStretchFactor(float scale)
{
if (scale < 1.5)
{
return scale;
}
else if (scale < 2.5)
{
return scale / 2;
}
else if (scale < 3.5)
{
return scale / 3;
}
else//預設為3倍圖拉伸
{
return scale / 3;
}
}
以上就是DPI適配方案的大致思路了,因為篇幅原因沒有針對每一個widget和layout進行詳細說明,有需要的可以私聊。
四、相關文章
Qt之高DPI顯示器(一) - 解決方案整理
PPI vs. DPI: 有什麼區別?
High DPI Desktop Application Development on Windows
PROCESS_DPI_AWARENESS Enumeration
SetProcessDPIAware function:Win Vista開始支援的介面
SetProcessDpiAwareness function:Win8.1開始支援的介面
關於Windows高DPI的一些簡單總結
如何開發新的Qt 5.7高DPI每監視器DPI感
值得一看的優秀文章:
- 財聯社-產品展示
- 廣聯達-產品展示
- Qt定製控制元件列表
- 牛逼哄哄的Qt庫
如果您覺得文章不錯,不妨給個打賞,寫作不易,感謝各位的支援。您的支援是我最大的動力,謝謝!!!
很重要--轉載宣告
本站文章無特別說明,皆為原創,版權所有,轉載時請用連結的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。