原文連接:Qt之高DPI顯示器(二) - 自適配解決方案分析html
最近一直在處理高DPI問題,也花費了很多功夫,前先後後使用了多種解決方案,各類方案也都有利弊,筆者最終採用了自適配方案,雖然複雜一些,可是結果可控。這裏把處理的過程記錄下來,留給有一樣需求的同窗正則表達式
上一篇文章Qt之高DPI顯示器(一) - 解決方案整理講述了筆者處理高DPI顯示的一系列分析過程,爲了更好的閱讀和排版,其中有一些實驗方案沒有具體寫出,即便寫出來也沒有多大用處,並且會影響你們閱讀。shell
本篇文章將會接着上一篇文章的最後一小節-自適配高DPI
進行講解,因爲內容比較多,並且整個解決方案代碼量也會至關大,所以文章中也只會涉及到整個DPI適配架構的核心和一些關鍵代碼,有疑問歡迎提問windows
上一篇文章提到了T窗口,那麼什麼是T窗口呢!下面咱們來具體分析。api
這裏筆者貼一個適配完成之後的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適配方案其實原理很簡單,沒有想象中那麼複雜,方案也是中規中矩,其中遵照如下這麼幾條大的原則app
是否是提及來很簡單,可是要實現這麼一個流程仍是有一些難度的,首先考慮的就是效率,若是作完效率跟不上那麼一切都是瞎扯。框架
爲了更好的效率,筆者也是作了不須要的優化,優化的內容不在本篇文章中進行討論,後續會單獨分出一篇文章說明ide
下面是兩個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()
dpi變化時回調類,當dpi發生變化時,經過該接口類中的ScaleChanged方法進行處理變更,好比說第一小節中的TWidget類,咱們也重寫了這個接口,在該接口中咱們對窗體進行了大小適配和佈局適配
對象聲明中的函數聲明使用了宏進行包裝所以沒有直接顯示出來
void TWidget::ScaleChanged(float scale) { DEFINTE_SCALE_RESIZE(Widget); if (m_pLayout) { m_pLayout->ScaleChanged(scale); } AdjustReiszeHandle();//若是窗體正在被拖拽須要適配拖拽的位置 }
IDPIHelper是整個DPi適配的核心模塊,他負責整個DPI調度的核心功能,包括:DPI改變檢測、獲取指定window窗體縮放比、獲取指定window窗體的qss內容和獲取指定數值在不一樣DPI下的實際數值等。除過以上核心接口之外,筆者爲了優化DPI適配效果,還增長了一系列優化接口,主要是針對用戶主機只有一種DPI時所做的性能提高。
因爲篇幅緣由,這裏把一些關鍵實現節點列出來
一、dpi變化入口
以下是dpi發生變化實現接口,函數中幹了三件事
懸浮窗體指沒有佈局的窗體,當懸浮窗體的父窗體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; }
二、獲取指定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); }
大多數的窗體都是在佈局中完成的,可是也有一小部分的窗口不在佈局中,須要單獨去適配,這個時候就須要使用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咱們須要適配四個項目,分別是窗口大小、字體大小、間距和圖標,那麼接下來就開始咱們的分析過程
要適配軟件窗口大小,咱們總共須要重寫以下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;
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; }
Qt中的佈局有2中方式能夠設置,能夠在代碼中經過接口設置,也能夠經過qss進行設置,固然了這兩種狀況都須要適配。
佈局的margin
記錄調用了哪些設置大小的函數,在dpi發生變化時從新設置一遍,相似於窗口大小變化時所做調整
if (testflag("margin")) { setContextMargin(...); }
padding和margin
方式和放大字體同樣,能夠經過統一的時機去處理
讀取原有qss文件,使用正則表達式生成scale版本的新qss文件。
圖標替換是一個相對來講比較複雜的事情,這裏有必要細說一下。
首先是工程中須要額外添加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進行詳細說明,有須要的能夠私聊。
High DPI Desktop Application Development on Windows
PROCESS_DPI_AWARENESS Enumeration
SetProcessDPIAware function:Win Vista開始支持的接口
SetProcessDpiAwareness function:Win8.1開始支持的接口
值得一看的優秀文章:
![]() |
![]() |
很重要--轉載聲明