最近一直在处理高DPI问题,也花费了不少功夫,前前后后使用了多种解决方案,各种方案也都有利弊,笔者最终采用了自适配方案,虽然复杂一些,但是结果可控。这里把处理的过程记录下来,留给有同样需求的同学
一、回顾上一篇文章Qt之高DPI显示器(一)-解决方案整理讲述了笔者处理高DPI显示的一系列分析过程,为了更好的阅读和排版,其中有一些实验方案没有具体写出,即使写出来也没有多大用处,而且会影响大家阅读。
本篇文章将会接着上一篇文章的最后一小节-自适配高DPI进行讲解,由于内容比较多,而且整个解决方案代码量也会相当大,因此文章中也只会涉及到整个DPI适配架构的核心和一些关键代码,有疑问欢迎提问
上一篇文章提到了T窗口,那么什么是T窗口呢!下面我们来具体分析。
这里笔者贴一个适配完成以后的TWidget类,大家可以先分析分析,也可以猜猜看,每一处代码的具体含义。所有代码细节笔者后边会具体分析每一处细节
//函数声明//xxx.h#defineCreateTWidget()CreateObject(Widget)classTWidget:publicQWidget,publicICallDPIChanged{Q_OBJECTpublic:TWidget(floatscale,QWidget*parent=nullptr);TWidget(QWidget*parent=nullptr);//不建议使用TWidget(QWidget*parent,Qt::WindowFlagsf);//不建议使用~TWidget();public://重写大小变化相关函数DECLARE_RESIZE();voidsetLayout(QLayout*layout);public://QWidgetvirtualboolnativeEvent(constQByteArrayeventType,void*message,long*result)override;//ICallDPIChangedDECLARE_DPI();//TWidgetvirtualvoidAdjustReiszeHandle();DECLARE_DPI_SYMBOL;protected:WidgetResizeHandlerresize_handler;//用于支持放大缩小拖拽等功能private:TigerUILib::ReiszeActionsm_sizeActions;QSizem_size;QSizem_minimumSize;QSizem_maximumSize;ICallDPIChanged*m_pLayout=nullptr;//DPI发生变化时通知布局};//函数实现//xxx.cppTWidget::TWidget(floatscale,QWidget*parent):QWidget(parent),dpi_scale(scale){}TWidget::TWidget(QWidget*parent/*=nullptr*/):QWidget(parent){dpi_scale=WINDOW_SCALE;}TWidget::TWidget(QWidget*parent,Qt::WindowFlagsf):QWidget(parent,f){dpi_scale=WINDOW_SCALE;}TWidget::~TWidget(){DPIHelper()-RemoveDPIRecord(WINDOW_WINID);}voidTWidget::setLayout(QLayout*layout){WIDGET_RELEATE_LAYOUTS(layout);__super::setLayout(layout);}DEFINE_RESIZE(Widget);DEFINE_DPI(Widget);boolTWidget::nativeEvent(constQByteArrayeventType,void*message,long*result){MSG*pMsg=reinterpret_castMSG*(message);switch(pMsg-message){caseWM_DPICHANGED:{DWORDdpi=LOWORD(pMsg-wParam);WIdid=WINDOW_WINID;if(DPIHelper()-DPIChanged(dpi,id)){ScaleChanged(DPIHelper()-GetDPIScale(id));RefrushSheet(this,id);}}}return__super::nativeEvent(eventType,message,result);}voidTWidget::ScaleChanged(floatscale){DEFINTE_SCALE_RESIZE(Widget);if(m_pLayout){m_pLayout-ScaleChanged(scale);}AdjustReiszeHandle();}voidTWidget::AdjustReiszeHandle(){if(resize_handler.isWidgetMoving()){resize_handler.dpiChanged(WINDOW_SCALE);}}
二、框架说明
用一段话描述一下DPI适配方案?
答:笔者提到的DPI适配方案其实原理很简单,没有想象中那么复杂,方案也是中规中矩,其中遵守以下这么几条大的原则
首先就是window窗体自己去监测自身所在屏幕DPI发生变化,发生变化时通知布局进行缩放
局部缩放后,然后对自身所包含的widget窗体和布局进行缩放
不在布局中的窗体需要单独去控制缩放
是不是说起来很简单,但是要实现这么一个流程还是有一些难度的,首先考虑的就是效率,如果做完效率跟不上那么一切都是瞎扯。
为了更好的效率,笔者也是做了不需要的优化,优化的内容不在本篇文章中进行讨论,后续会单独分出一篇文章说明
下面是两个DPI适配框架的核心接口类,分别是DPI发生变化时的回调接口类和DPi管理接口类
structICallDPIChanged{virtualvoidScaleChanged(floatscale)=0;virtualWIdGetWID()const=0;virtualvoidSetScale(floatscale)=0;};#defineSTANDARD_DPI96.0structIDPIHelper{virtualboolDPIChanged(unsignedshort,WId)=0;virtualvoidRemoveDPIRecord(WId)=0;//移除指定native窗体的DPI记录一般用于native窗体析构时virtualfloatGetDPIScale(WId)const=0;virtualfloatGetOldDPIScale(WId)const=0;virtualQStringGetStyleSheet(WId)const=0;//获取指定DPI下的样式表virtualfloatGetScaleNumber(float,WId)const=0;//获取指定DPI下的数值缩放后数值virtualQListWIdGetAllWindowID()const=0;//获取所有自己加载过皮肤的窗口ID//优化接口主要是为了适配用户主机只有一种DPI时使用virtualboolIsOnlyOneDPI()const=0;//获取用户主机是否只有一种DPIvirtualvoidRefrushDPIRecords()=0;//显示器数量发生了变化刷新历史显示器DPI记录virtualvoidSetDefaultScale(floatscale)=0;//设置缺省DPI值当显示器dpi只有一种时刷新virtualfloatGetDefaultScale()const=0;//获取缺省DPI缩放值只有当机器上所有的显示器为统一dpi时起作用};IDPIHelper*GetDPIHelper();#defineDPIHelper()GetDPIHelper()
1、ICallDPIChanged
dpi变化时回调类,当dpi发生变化时,通过该接口类中的ScaleChanged方法进行处理变动,比如说第一小节中的TWidget类,我们也重写了这个接口,在该接口中我们对窗体进行了大小适配和布局适配
对象声明中的函数声明使用了宏进行包装因此没有直接显示出来
voidTWidget::ScaleChanged(floatscale){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发生变化时,相应的悬浮窗体也需要进行适配
boolCDPIHelper::DPIChanged(unsignedshortdpi,WIdid){#ifndefHIGHDPI_ENABLEreturnfalse;#endiffloatscale=dpi/STANDARD_DPI;RefrushDPIRecords();if(m_pWindowScale.contains(id)){if(m_pWindowScale[id]==scale){returnfalse;}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);returntrue;}
2、获取指定DPI下的qss内容
voidCDPIHelper::RefrushTimesSheet(Skin::SKIN_TYPEskin,WIdid){floatscale=GetDPIScale(id);inttimes=(int)(scale+0.);//几倍图//如果基础qss不存在则需要从硬盘中读取//读取时按照向上取整进行读取qss文件//如果高分屏qss不存在则读取一倍qss文件if(m_StyleSheets[skin].size()times){m_StyleSheets[skin].resize(times);}std::wstringfilePath=ImagePath::GetSkinFilePath(skin,m_strQssFile.toStdWString(),times);if(QFile::exists(QString::fromStdWString(filePath))==false){filePath=ImagePath::GetSkinFilePath(skin,m_strQssFile.toStdWString());}QFileqss(QString::fromStdWString(filePath));qss.open(QFile::ReadOnly);if(qss.isOpen()){QStringbtnstylesheet=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发生变化时通知悬浮窗口支持如下类型的悬浮窗口:TFrameTPushButtonTLabelTTableViewTWidgetTDialogTMainWindow*/classCFloatingWidgetMgr:publicQObject{Q_OBJECTpublic:staticCFloatingWidgetMgr*getInstance();public:voidaddWidget(QWidget*widget);//dpihelpercallvoiddpiChanged(WIdid,floatscale);private:QSetICallDPIChanged*m_pWidgets;};
悬浮窗体适配高DPI也很简单,只需要把自己加入到悬浮窗体管理器中即可,是不是也很简单。
CFloatingWidgetMgr::getInstance()-addWidget(xxx);三、方案分析
既然我们要重写Qt控件的非virtual接口,那么这个行为在C++语法上应该叫覆盖,要想调用我们覆盖的函数,使用多态肯定是不行的,聪明的你肯定也想到了,我们在使用界面类时,只能使用T打头的控件类声明对象,这样就会调用我们覆盖后的接口
上一篇文章大致说过,要自适配高DPI我们需要适配四个项目,分别是窗口大小、字体大小、间距和图标,那么接下来就开始我们的分析过程
1、窗口大小要适配软件窗口大小,我们总共需要重写如下14个和大小相关函数,而且这只是大小相关的函数,也就是QWidget的接口,其他更复杂的接口需要针对具体的类去重写
voidresize(intw,inth);voidresize(constQSize);voidsetFixedHeight(intw);voidsetFixedWidth(intw);voidsetFixedSize(intw,inth);voidsetFixedSize(constQSizes);voidsetMinimumSize(constQSize);voidsetMinimumSize(intminw,intminh);voidsetMinimumHeight(intminh);voidsetMinimumWidth(intminw);voidsetMaximumSize(constQSize);voidsetMaximumSize(intmaxw,intmaxh);voidsetMaximumHeight(intminh);voidsetMaximumWidth(intminw);
Qt的界面类我粗略估计了下,至少有几十个,如果每一个类都需要去适配,那么工作量可想而知,因此笔者想了一个办法,做了一系列宏,像下面代码这样,只需要在我们想要适配的类中添加宏即可
//函数声明#defineDECLARE_RESIZE()\voidresize(intw,inth);voidresize(constQSize);voidsetFixedHeight(intw);\voidsetFixedWidth(intw);voidsetFixedSize(intw,inth);voidsetFixedSize(constQSizes);\voidsetMinimumSize(constQSize);voidsetMinimumSize(intminw,intminh);\voidsetMinimumHeight(intminh);voidsetMinimumWidth(intminw);\voidsetMaximumSize(constQSize);voidsetMaximumSize(intmaxw,intmaxh);\voidsetMaximumHeight(intminh);voidsetMaximumWidth(intminw);\
实际使用过程类似第一小节那样,非常简单。
函数声明有了,接下来就是函数实现,方法类似,笔者还是写了一个宏来适配相关放大函数,代码下下面这样
//函数实现#defineDEFINE_RESIZE(name)\voidT##name::resize(intw,inth){m_sizeActions
=TigerUILib::RA_Resize;floatscale=dpi_scale;m_size=QSize(w,h);;__super::resize(m_size.width()*scale,m_size.height()*scale);}\voidT##name::resize(constQSizesize){m_sizeActions
=TigerUILib::RA_Resize;floatscale=dpi_scale;m_size=size;__super::resize(m_size*scale);}\voidT##name::setFixedHeight(inth){m_sizeActions
=TigerUILib::RA_FixedHeight;floatscale=dpi_scale;m_size.setHeight(h);__super::setFixedHeight(m_size.height()*scale);}\voidT##name::setFixedWidth(intw){m_sizeActions
=TigerUILib::RA_FixedWidth;floatscale=dpi_scale;m_size.setWidth(w);__super::setFixedWidth(m_size.width()*scale);}\voidT##name::setFixedSize(intw,inth){m_sizeActions
=TigerUILib::RA_FixedSize;floatscale=dpi_scale;m_size=QSize(w,h);__super::setFixedSize(m_size.width()*scale,m_size.height()*scale);}\voidT##name::setFixedSize(constQSizesize){m_sizeActions
=TigerUILib::RA_FixedSize;floatscale=dpi_scale;m_size=size;__super::setFixedSize(m_size*scale);}\voidT##name::setMinimumSize(constQSizesize){m_sizeActions
=TigerUILib::RA_MinimumSize;floatscale=dpi_scale;m_minimumSize=size;__super::setMinimumSize(m_minimumSize*scale);}\voidT##name::setMinimumSize(intw,inth){m_sizeActions
=TigerUILib::RA_MinimumSize;floatscale=dpi_scale;m_minimumSize=QSize(w,h);__super::setMinimumSize(m_minimumSize.width()*scale,m_minimumSize.height()*scale);}\voidT##name::setMinimumHeight(inth){m_sizeActions
=TigerUILib::RA_MinimumHeight;floatscale=dpi_scale;m_minimumSize.setHeight(h);__super::setMinimumHeight(m_minimumSize.height()*scale);}\voidT##name::setMinimumWidth(intw){m_sizeActions
=TigerUILib::RA_MinimumWidth;floatscale=dpi_scale;m_minimumSize.setWidth(w);__super::setMinimumWidth(m_minimumSize.width()*scale);}\voidT##name::setMaximumSize(constQSizesize){m_sizeActions
=TigerUILib::RA_MaximumSize;floatscale=dpi_scale;m_maximumSize=size;__super::setMaximumSize(m_maximumSize*scale);}\voidT##name::setMaximumSize(intw,inth){m_sizeActions
=TigerUILib::RA_MaximumSize;floatscale=dpi_scale;m_maximumSize=QSize(w,h);__super::setMaximumSize(m_maximumSize.width()*scale,m_maximumSize.height()*scale);}\voidT##name::setMaximumHeight(inth){m_sizeActions
=TigerUILib::RA_MaximumHeight;floatscale=dpi_scale;m_maximumSize.setHeight(h);__super::setMaximumHeight(m_maximumSize.height()*scale);}\voidT##name::setMaximumWidth(intw){m_sizeActions
=TigerUILib::RA_MaximumWidth;floatscale=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宏,如下代码,就不解释了一看应该都会明白
#defineDEFINTE_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)){QSizenewSize=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.然后取整数部分。
QStringQtTigerHelper::ScaleSheet(constQStringsheet,floatscale){if(sheet.isEmpty()){returnsheet;}//1倍图时不需要做任何处理if(scale==1.0){returnsheet;}//放大字体QStringtempStyle=sheet;QRegExprx("\\d+px",Qt::CaseInsensitive);rx.setMinimal(true);intindex=-1;while((index=rx.indexIn(tempStyle,index+1))=0){intcapLen=rx.cap(0).length()-2;QStringsnum=tempStyle.mid(index,capLen);snum=QString::number(qRound(snum.toInt()*scale));tempStyle.replace(index,capLen,snum);index+=snum.length();if(indextempStyle.size()-2){break;}}returntempStyle;}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
压缩系数:绘制图片的时候这里有一个小窍门,当我们绘制缩放比为小数情况时,需要使用距离较近的整数图片进行压缩绘制,这样的情况我们就需要使用压缩系数进行动态调整绘制图片的大小
floatImagePath::GetStretchFactor(floatscale){if(scale1.5){returnscale;}elseif(scale2.5){returnscale/2;}elseif(scale3.5){returnscale/3;}else//缺省为3倍图拉伸{returnscale/3;}}
以上就是DPI适配方案的大致思路了,因为篇幅原因没有针对每一个widget和layout进行详细说明,有需要的可以私聊。
预览时标签不可点收录于话题#个上一篇下一篇